The next step of the redux journey was to build out react-redux. Feel free to have a more detailed
look at my code here. React Redux allows us to
inject the state into our components. I also decided to inject in commands which are in charge of dispatching actions
and making an API call. Dispatching actions will update our store. I’ll explain this further in another blogpost, but right now we
want to inject in the state and a container that contains commands into our components. Inject in these values into props allows us to have a greater chance
of creating a stateless component. In this example there are the types for State
and AppContainer
.
export type AppContainer = {
photoCommands: PhotoCommands
};
export type State = {
photos: PhotoState
};
export type PhotoState = {
isLoading: boolean,
photo?: string
};
We’re going to utilize react context in order to do that.
Context is part of react. It’s an object that is available to components at any part of the component tree. More info can be found here.
This allows us to avoid having to pass down state from the top level component and through the entire chain. Instead we can utilize the context and add the state and the container
straight into the context. I decided to create a component that will add the state and container to the context. I call it Provider
.
type Props<TContainer, TState> = {
container: TContainer
store: Store<TState>
}
export const ContainerContext = React.createContext({});
export const StoreContext = React.createContext({});
export default class Provider<TContainer, TState> extends React.Component<Props<TContainer, TState>, {}> {
public render() {
return (
<StoreContext.Provider value={this.props.store}>
<ContainerContext.Provider value={this.props.container}>
{this.props.children}
</ContainerContext.Provider>
</StoreContext.Provider>
)
}
}
The top level react component now just needs to be wrapped in Provider
and then our state and container will be available to use through context
.
export default class App extends React.Component {
private setup: Setup;
constructor(props: any) {
super(props);
this.setup = new Setup();
}
public render(): JSX.Element {
const store = this.setup.executeState();
const container = this.setup.executeContainer(store);
return (
<Provider
store={store}
container={container}
>
<Router>
{routes}
<Landing />
</Router>
</Provider>
);
}
}
To consume the context we would call
<StoreContext.Consumer>
{store => {
// component here
}
}
</StoreContext.Consumer>
and
<ContainerContext.Consumer>
{container => {
// component here
}
}
</ContainerContext.Consumer>
In order to try to contain this complexity in one spot, a component can pass in arguments to a function about which parts it needs from the state and container. The function can then
inject those into the caller component. Let’s say the component has these Props
type Props = {
isLoading?: boolean,
photo?: string,
photoCommands?: PhotoCommands
}
isLoading
and photo
come from the state and photoCommands
comes from the container. The component that has these props is called PhotoTest
. We’re going to create a helper function called
connect
which will take in the target component - PhotoTest
, a function that will inject in specific props from the container, and a function that will inject in the specific props from the state.
connect
will return a new component that has the appropriate values of the props injected in.
export default connect(
PhotoTest,
injectCommands<AppContainer, Props> (c => ({
photoCommands: c.photoCommands
})),
injectState<State, Props>((state, props) => {
const isLoading = state.photos.isLoading
const photo = state.photos.photo
return {
isLoading,
photo
}
})
)
Here injectCommands
is in charge of taking the Target
component (PhotoTest
) and injecting in photoCommands
as a prop for it use. injectState
is in charge of taking the Target
component
(PhotoTest
) and injecting in isLoading
and photo
.
Here comes the confusing bit. injectCommands
and injectState
are higher order functions themselves. They take in a function that maps the container to props in the Target
component. It returns another function
that takes in the Target
component and returns a new component. This new component has the existing values for the props of the Target
component, but it also fills in values for the props that are needed from the container
and state respectively.
Here’s the implementation for injectState
.
export type Connector<TProps> = (c: ComponentClass<TProps>) =>
ComponentClass<TProps>
export type StateInjector<TGlobalState, TProps> = (state: TGlobalState, props: TProps) =>
Partial<TProps>
/*
* This returns a function that takes in a component and injects in
* the appropiate state variables into the component as props
*/
export function injectState<TGlobalState, TProps>(func: StateInjector<TGlobalState, TProps>): Connector<TProps> {
return (Target: ComponentClass<TProps>) => {
return class injectStateHOC extends React.Component<TProps, ComponentState> {
public render(): JSX.Element {
return (
<StoreContext.Consumer>
// values from the state are passed in here as props
{store => (<Target
{...this.props}
{...this.inject(store as Store<TGlobalState>)}
/>)
}
</StoreContext.Consumer>
)
}
private inject(store: Store<TGlobalState>): Partial<TProps> {
if(!store) {
throw 'Store not found'
}
/*
// this is the function specified in the target component. Ex:
// (state, props) => {
// const isLoading = state.photos.isLoading
// const photo = state.photos.photo
// return {
// isLoading,
// photo
// }
// }
*/
return func(store.getState(), this.props)
}
}
}
}
The implementation for injectCommands
is practically identical.
export type CommandsInjector<TContainer, TProps> = (container: TContainer) =>
Partial<TProps>
export function injectCommands<TContainer, TProps>(func: CommandsInjector<TContainer, TProps>): Connector<TProps> {
return (Target: ComponentClass<TProps>) => {
return class injectHOC extends React.Component<TProps, ComponentState> {
public render(): JSX.Element {
return (
<ContainerContext.Consumer>
// values from the container are passed in here as props
{container => (<Target
{...this.props}
{...this.inject(container as TContainer)}
/>)
}
</ContainerContext.Consumer>
)
}
private inject(container: TContainer): Partial<TProps> {
if (!container) {
throw 'Container not found'
}
/*
// this is the function specified in the target component. Ex:
// (c => ({
// photoCommands: c.photoCommands
// })),
*/
return func(container)
}
}
}
}
So now we’re able to take a Target
component and give it values from the state and container through the context
.
Based on the fact that injectState
and injectCommands
follow the same higher order function pattern (returns a function that takes in the Target
component).
The connect
function now has a simple implementation using reduce
.
export function connect<TProps>(
targetComponent: ComponentClass<TProps>,
...connectors: Connector<TProps>[]
): ComponentClass<TProps> {
return connectors.reduce((component, nextConnector) => nextConnector(component), targetComponent);
}
We now have a targetComponent
and connectors
, which in this case is injectState
and injectCommands
. Both of these functions return a new function that returns a new component. We can continue to pass along
the new component to the nextConnector
which will again return a new component. The beauty of this pattern is nothing is mutated. New components are created and added to.
This certainly took me a bit of time to think through and sit with. So it’s ok if it doesn’t click right away! But it’s a powerful pattern that allows us to greatly simplify our code and inject in props from the state and commands into our component.