EdTech developers provide solutions that offer tremendous opportunities for the education industry. Using today’s technology they are able to create an immersive learning experience in a compact form of […]
If you have ever worked with React, you probably heard about Redux. Redux is a predictable state container for JavaScript Apps. In this piece, I would explain how it works and what is flux architecture. Why Flux Architecture? Because this is the group of paradigms on which Redux is based. The goal of this article is to prepare you to use the full potential of Redux by understanding the way it works and how it’s made. Through this article, I would extend the topic by building my own mini-Redux to show you a practical way of using Flux Architecture.
The main rule of FLUX is one way data flow in our application. Data migrates through the action dispatcher to the store (global state) from which the data goes to the view – React components. Usually, actions are triggered by elements of application like inputs or buttons.
If you want to learn more about FLUX I suggest discovering documentation made by Facebook’s engineers. They invented and shared this approach as well as a brilliant presentation about it.
Thanks to the theoretical knowledge, we can make our own implementation of Flux architecture which would enhance our React app. We would need to use parts like store, dispatchers, and access to the stored data which we would get thanks to the subscription.
PubSub is another pattern, which redux inventors used while building their own library. So as you may already recognize they haven’t invented anything new
In case of this particular pattern, we won’t dig deep into it’s theoretical background. Instead of it let’s get straight to the way its implemented.
Let’s start from creating Start class. It is responsible for storing the state.
class Store { constructor(reducer, initialState = {}) { this.state = initialState || {}; this.reducer = reducer; } } const reducer = (state, action) => { return state; }; const initialState = { user: { name: "John Doe", money: 0 } }; const store = new Store(reducer, initialState);
Now we have a store which can be initialized with reducer and initial state. The next part which we need is methods. Methods which would allow as to trigger actions and return the new state. However, we would need a way to access this data and the possibility to change them. The publish-subscribe pattern would help us in this.
class Store { constructor(reducer, initialState) { this.state = initialState || {}; this.reducer = reducer; this.listeners = []; } subscribe(fn) { this.listeners.push(fn); } dispatch(action) { this.state = this.reducer(this.state, action); this.listeners.map(fn => fn()); } getState() { return this.state; } }
Thanks to these additional lines of code, we have the Store equipped with four basic methods
this.listeners = []; subscribe(fn) { this.listeners.push(fn); }
We create a table with listeners in which we would store all of the functions which would monitor changes in our state. We are able to achieve it thanks to the subscribe method which takes the name of the function as a parameter.
store.subscribe(() => { console.log('state has been updated') console.log(store.getState()); })
It allows us to react to any change of our state.
subscribe(fn) { this.listeners.push(fn); return { unsubscribe: () => { this.listeners = this.listeners.filter(el => el !== fn); } } }
Let’s add an option to end subscription through unsubscribe too.
this.subscribtion = store.subscribe(() => {}) // listening store’s state changes this.subscribtion.unsubscribe() // cancel listening
It helps to optimize the usage of browser memory because when we do not need to monitor state, we can delete our function from a table of listeners by filtering it out. For example, you can execute subscription in React’s componentDidMount and then stop monitoring when the component would unmount and trigger unsubscribe in componentWillUnmount.
We also create a dispatch function. It uses an object with a type of action as a parameter.
this.state = this.reducer(this.state, action);
In dispatch function, we trigger reducer which returns new state based on a triggered action.
this.listeners.map(fn => fn());
In dispatch, we have to provide all our listeners with any changes which occurred. We do it through a simple iteration on the table of listeners by triggering any function, which is saved in this table.
As you can see in this example, I implemented the store by listening to its changes in componentDidMount by saving our store to the state. Through this way, we inform our app about changes which allows updating our component. I already mentioned this, but it is extremely important – every time you stop using a subscription you should delete it. In this example, I implemented it in componentWillUnmount. This rule is not only accurate and important while working with redux but also in any publish-subscribe pattern based library i. E. rxjs.Unfortunately getting data from global state to our react app isn’t as convenient as it could be. That’s why we would implement a higher order component – connect. It provides us with the possibility to get the state as well as triggering actions from React component.
We would start implementing it from creating a Provider which would be responsible for providing all of the components in our application with the state.
const StoreContext = React.createContext(null);
I use react Context API to do it. In the beginning, I have to create context:
class StoreProvider extends React.Component { render() { const Context = StoreContext; return ( <Context.Provider value={{ ...this.props.store }}> {this.props.children} </Context.Provider> ); } }
I created the Provider, which takes store as a prop:
const store = new Store(reducer, initialState) function App () { return ( <StoreProvider store={store}> <h1>App</h1> </StoreProvider> ) }
Lately, while using Context, we would have easy access to our state in react components thanks to this particular part.
However, this Provider lacks listening to changes of the state which occurs after an action is triggered. Let’s resolve this issue now.
class StoreProvider extends React.Component { constructor(props) { super(props); this.state = { state: props.store.getState() }; this.subscribtion = null; } componentDidMount() { const {store} = this.props; this.subscribtion = store.subscribe(() => { this.setState({ state: store.getState()}); }); } componentWillUnmount() { this.subscribtion.unsubscribe(); } render() { const Context = StoreContext; return ( <Context.Provider value={{ ...this.props.store, ...this.state }}> {this.props.children} </Context.Provider> ); } }
In the componentDidMount method, we are listening to any changes in the state of our store. In provider, we store state in the local state of the component, so any time we trigger the setState method component would update the value of state and pass it to the <Context.Provider>. Lately, we can use values in <Context.Consumer>.
If you aren’t familiar with Context API, I suggest reading React’s documentation. It contains lots of detailed explanation on this topic: https://reactjs.org/docs/context.html#api
Our connect would contain two functions:
const mapStateToProps = (state, props) => {}, const mapDispatchToProps = dispatch => ({})
In the first one we are going to map the global state to the component’s props:
const mapStateToProps = (state) => ({ color: state.color })
It helps us to access the value of the global state while we are referring to this.props.color inside of react component.
const mapDispatchToProps = (dispatch) => ({ changeColor: () => dispatch({ type: "CHANGE_COLOR" }) })
The second function we can impact the change of global state through dispatching action from the component by using props. In this case, we do it by using this.props.changeColor() inside of the component.
We also need to pass component to our HOC. Finally, it would look like this:
const ConnectedApp = connect(mapStateToProps, mapDispatchToProps)(App)
export const connect = () => WrappedComponent => { return class extends React.Component { render() { return <WrappedComponent {...this.props} /> } } }
When we have a HOC which takes a component and return it without doing anything useful, we can focus on connecting our component to the global store. We download data from the store using Consumer from the React Context API, which we’ve already used in Provider.
import { StoreContext } from "./provider"; export const connect = () => WrappedComponent => { return class extends React.Component { const Context = StoreContext; render() { return ( <Context.Consumer> {({ state, dispatch }) => { return <WrappedComponent {...this.props} /> } <Context.Consumer> ) } } }
Using Context.Consumer provides us with access to the global state and dispatch method. We would use both of them while implementing mapStateToProps and mapDispatchToProps inside of our component.
import { StoreContext } from "./provider"; export const connect = ( mapStateToProps = (state, props) => {}, mapDispatchToProps = dispatch => {} ) => WrappedComponent => { return class extends React.Component { const Context = StoreContext; render() { return ( <Context.Consumer> {({ state, dispatch }) => { const mappedState = mapStateToProps(state, this.props); const mappedDispatch = mapDispatchToProps(dispatch); return ( <WrappedComponent {...this.props} {...mappedState} {...mappedDispatch} /> ) } <Context.Consumer> ) } } }
Thanks to this, we can pretty easily refactor our whole react app and add state to it by using connect instead of directly referring to the methods inside of the store.
I hope that this article in which we created our own implementation of React-Redux would help you to understand the basics of working with this library. Moreover, I hope that you would be able to use it in your own project. That’s why I encourage you to look in Redux and React-Redux source code and maybe refining this implementation.