Redux is a very useful state-management library for UI development. When you are dealing with React alone and your app starts to get really complex, managing state becomes very difficult.
In React, each component is responsible for handling its own state. When state from some component needs to be shared with a sibling component, for instance, it needs to get lifted up to the parent component, which will send it through props to each of its children. Now, if two components very distant apart need to share some state, things get really messy. Now a bunch of potentially unrelated components need to pass that piece of state through the tree until it reaches its final destination. Handling it all gets confusing and introduces the possibility of adding more bugs to the application.
That's where Redux comes in.
reducer
for?Take some existing data and some action, modify and return that existing data based upon the contents of an action.
A reducer gets called with two arguments: the current state of the app, and an action to change that state. It returns the modified state.
Usually a React with Redux project is organized like so:
- src/
- actions/
- components/
- reducers/
- index.js
The components
folder is your vanilla React folder with all your app components. Inside of your actions/
folder your will put files containing all your Action Creators, and in reducers
you put - guess what - the reducers!
You will use index.js
for configuring stuff. You will usually see something like this at your root index.js
file:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
All of our app needs to be wrapped inside that Provider
component from the react-redux
library. Also, this is where we create our store where all of our app data will live. You will probably have something like this:
ReactDOM.render(
<Provider store={createStore(reducers)}>
<App />
</Provider>,
document.querySelector('#root')
);
See that we are creating a store with all of your reducers (which we need to import from our reducers/
folder) and passing it as a prop to Provider
. Now, any component inside our application can get access to the Redux store.
mapStateToProps
?This name is just a convention. That's a function you define inside your component file, and what is does is take the Redux state and map it to props that will be sent into our component. We use it to configure the connect
function (which will be explained bellow).
If we do something like this:
const mapStateToProps = (state) => {
console.log(state);
return state;
};
We would see all the state of our app. But our individual component doesn't care about that. There's a specific subset of data that our component needs access to, and so we write this mapStateToProps
function as a way to "filter" this state and send it to our component just what it needs to know, mapped as a prop. Nice, isn't it?
For instance, we could do something like this:
class SampleComponent extends React.Component {
//if we check this.props, it should contain = { importantData: state.data }
}
const mapStateToProps = (state) => {
return { importantData : state.data }
};
In the end, this function does exactly what the name says.
The mapStateToProps
function gets called every time that we change our Redux state or anytime we rerun our reducers and create some new state object.
Also, you may need to get access to some components props inside your mapStateToProps
. Fortunately, this function is called with a second argument, which is a copy of the props passed to the component:
const mapStateToProps = (state, ownProps) => {
return { importantData : state.data }
};
connect
worksWhen inside a component, you may want to access that Redux store that we've created in our app. To do that, we need to make use of the connect
function from inside the React-Redux library. That function has a pretty strange syntax when you first look at it:
class MyComponent extends React.Component {
render() {
return <div> My Component </div>;
}
}
export default connect(mapStateToProps)(MyComponent);
That connect()(MyComponent)
is not as cryptic as it may seem. It's just that the connect()
function is actually a function that returns another function, and it's to this returned function that we send our component as a parameter (okay, that may have sounded a little bit cryptic, but bear with me here).
The connect
function will take a mapStateToProps
function as an argument to configure itself. It can also receive an Action Creator as a second argument:
import { myActionCreator } from '../actions';
export default connect(mapStateToProps,
{ myActionCreator: myActionCreator })(MyComponent);
Notice that the second argument to connect
is an object with key myActionCreator
(it could be anything, I'm using this as an example), and value being the Action Creator that we've imported from the /actions
folder. We could use some ES2015 syntax and write key and value like the following:
import { myActionCreator } from '../actions';
export default connect(mapStateToProps, { myActionCreator })(MyComponent);
Here, we have an object with both key and value with name myActionCreator.
If you check your component props now, you will notice that you have a myActionCreator
function being sent as a prop. That's your action creator. If you call it, it will automatically take the action that gets returned and send it to Redux dispatch function.
Let's say we are using Redux in our application and we need to fetch data from some API. In Redux, the common procedure would be:
That means that you can't directly make network requests inside action creators.
Let me show an example of bad code:
export const myActionCreator = async () => {
const response = await doApiCall();
return {
type: 'FETCH_DATA',
payload: response
};
}
Surely it looks correct, right? After all, we are returning a plain object from our Action Creator. But that's not the case, and that's because of the async
and await
syntax we are using to handle our API call. If you use a tool like Babel to transpile this code to the actual Javascript code that's going to run in the browser, you will
notice that those async
and await
keywords will end up looking really nasty, and your function ends up not returning what you think it's returning. It will not be a
plain object.
All this troube just because this is an asynchronous Action Creator. For this kind of stuff you are required to install some middleware.
We can all agree that the flow of data in a Redux application usually looks something like this:
dispatch
dispatch
sends data to reducer
reducer
creates a new stateFor an asynchronous action creator, it will look like this instead:
dispatch
dispatch
forwards action to middleware
middleware
sends data to reducer
reducer
creates a new stateSo, let's remember how to use those middlewares
inside our Redux app.
middlewares
with Action CreatorsOkay, what is a middleware
after all?
Redux Thunk
is a famous middleware
for dealing with issues such as those we've discussed in the previous section.
You may remember that the rules for building an Action Creator are:
Redux Thunk will add one option to those:
Besides that, if you return a function, Redux Thunk will call that function for you. If you return an object, it will simply pass the object normally to the reducers.
To configure some middleware (it can be anything, not necessarily Redux Thunk), you can do this:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(reducers, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#root')
);
Now, inside your Action Creators, you would do something like this:
export const myActionCreator = () => {
return async function(dispatch, getState) {
const response = await doApiCall();
dispatch({ type: 'FETCH_DATA', response: response })
};
};
You don't need to return an action. You will call dispatch
manually if you are instead returning a function from the Action Creator.
Notice, as well, that we are using the async/await syntax normally. But that's because, in the previous case, we end up returning a request object to the dispatch
function. Now, we are calling dispatch
manually inside our Action Creator, and the return value of the action creator itself is not being used.
We can use a shorter syntax, by the way:
export const myActionCreator = () => async dispatch => {
const response = await doApiCall();
dispatch({ type: 'FETCH_DATA', response: response })
};
Here we are using arrow functions, and since we didn't use getState
inside our function, it's not necessary to use it. Also, as we are returning a single value from myActionCreator
, we do not need the curly braces.
undefined
state
directlyUsually you need to make changes to your app state
data. That's a big part of a typical web application, right? Just remember to not alter it directly.
Examples of what not to do:
state.pop()
state.push()
state[0] = 'some stuff'
state.property = 10
Examples of what to do:
state.filter(el => el !== 0)
[...state, newElement]
state.map(el => { do something here... })
{...state, property = 10}
Notice that in the correct examples, we are not updating our state directly. We are not even touching the original state
object at all. Functions like filter
, map
or spread operators actually return a new object.
If you are dealing with Redux, this extension is actually pretty useful: