Redux stands out from the galaxy of libraries and frameworks by getting so many things absolutely right: A simple, predictable state model. An emphasis on functional programming and immutable data. A tiny, focused API...What's there not to like? Redux is a predictable state container for Javascript apps, in simple terms, it is a place where we can have all our state in a Javascript application. Redux is very tiny, (2kb, including dependencies) and it can run in different environments (client, server, native).
This tutorial will guide you through building a full-stack Redux and immutable application. Through the course of this tutorial, you will learn a few things:
• What is redux?
• What redux include?
• Implementation of redux
• Create redux store with ReactJs
• Adding actions and reducers to store
• Combine reducers
• Providers
• Connects
• Redux thunk
To better understand what redux is or what it does, let us first understand the problem redux helps us solve.
Without redux
Suppose we have a root component(father) in react (fig 1), and this component has two child components(children), who in turn have 4 sub components(grand-kids). Now, suppose we have to pass some data from root component(father) to child component(grand-kids), we have to first pass through the (children) component, likewise when we need to pass data from (children) to parent component, it needs to flow through a callback function, so that callback function must first be passed down to any components that want to call it to pass data up. Imagine this was a bigger application, we are bound somehow to face some prop drilling. With REDUX, its like a central store, just like our grocery store where everything is available, users go there and purchase. This redux store holds the state of our entire application where each component can directly call for the state and data from the redux store.
Structure of redux
Redux store-- The state of every redux application lives in the Redux Store, this store is central and accessible to every component. This store includes actions, actions are plain javascript objects just like this—{TYPE: “UPDATE_NAME”, NAME: kpose}. Then we have the REDUCERS, reducers are normal functions which get action. Put simply, reducers get information from the actions and update the state in the store. When using redux, there are three principles we should have at the back of our mind:
- Redux store is a single source of truth – The data/state of our application must be stored in one place which is the REDUX STORE.
- State is read-only----This means that we cant change/mutate our state directly, this is why we need reducers.
- Changes are made with pure functions—Reducers
Now, lets go to the real coding part, for that we use CodeSandbox where you can follow along, i recommend you keep the CodeSandbox in sync with this tutorial and actually type out the examples as you go along.
Create the Redux Store
In your CodeSandbox environment, expand the "Dependencies" section in the left pane, and click Add Dependency. Search for redux, add it, then click Add Dependency again and search for react-redux add redux-thunk, add them to your dependencies.
In a local project, you can install them with Yarn or NPM:
npm install --save redux react-redux redux-thunk
.
With redux installed, let's get our redux store ready. Redux comes with a function that create stores, and it’s called createStore.
In index.js, let’s make a store. Import createStore and call it like so:
import React from "react";
import ReactDOM from "react-dom";
import App from './App'
import {createStore} from 'redux';
const store = createStore();
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
and with that Redux store is ready, but we should notice some error in our console:
The createStore accepts a reducer which is a function, this it does not have at the moment. We have to provide a function that will return the state, that is what the reducer does. So let’s make a really simple one, pass it into createStore, and see what happens, but before that lets first understand what a reducer is and how it helps us.
What is a reducer in redux?
The reducer is a pure function that takes the previous state and an action, then returns the next or new state. Reducer function gets called with 2 arguments: it takes the current state, and an action, and returns the newState. Looks a lot like the signature of an Array.reduce reducer. Redux reducers work just like the function you pass to Array.reduce! :) Reducers reduce actions, the difference is that with Array’s reduce it happens all at once, but with Redux, it happens over the lifetime of your running app. Edit our *index.js **to use a *reducer like below:
import React from "react";
import ReactDOM from "react-dom";
import App from './App'
import {createStore} from 'redux';
const nameReducer = (state, action) => {
console.log('reducer', state, action);
return state
}
const store = createStore(nameReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__());
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Notice we also added our Redux DevTools extension, after you make this change, open up the console, you should see a message logged there, something like this:
Remember, i said the reducer’s job is to take the current state and an action then returns the new state, it has another job, too: It returns the initial state the first time it’s called. This is sort of like “bootstrapping” your app (more like giving it a starting point). Let us give our app an initial state variable and use the ES6 default argument syntax to assign it to state.
import React from "react";
import ReactDOM from "react-dom";
import App from './App'
import {createStore} from 'redux';
const initialState = {
name: "kpose"
};
function nameReducer(state = initialState, action) {
console.log('reducer', state, action);
return state;
}
const store = createStore(nameReducer);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
If you look at the console again, you’ll see it printed {name: kpose} as the value for state. That’s what we want.
Next, let's see how we can update our state.
Updating state
For us to be able to update our state, we need to dispatch an action. An action is a plain Javascript object, with a minimum of two objects, a type and a payload. Actions are very free-form things. As long as it’s an object with a type it’s fair game. In order to make an action DO something, you need to dispatch it. The store we created earlier has a built-in function called dispatch. Call dispatch with an action, and Redux will call your reducer with that action (and then replace the state with whatever your reducer returned, or it's payload).
Let’s try it out with our store.
...
const store = createStore(nameReducer);
//Dispatch action
store.dispatch({type: 'UPDATE_LASTNAME', payload:'Ganihu'})
store.dispatch({type: 'UPDATE_FIRSTNAME', payload:'Jude'})
console.log(store.getState())
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Every call to dispatch results in a call to your reducer!
Unfortunately, when we look at our console, we don't see any changes to our state, the name object in our state still has its value as "kpose" (its initial state). That’s because our reducer is not acting on those actions we dispatched. That’s an easy fix though. Let’s do that now.
Properly Dispatching Actions
To make actions actually do something, we need to write some code in the reducer that will inspect the type of each action and update the state accordingly. Let's do that now:
...
const nameReducer = (state = initialState, action) => {
console.log("reducer", state, action);
switch (action.type) {
case "UPDATE_LASTNAME":
return {
name: action.payload
};
case "UPDATE_FIRSTNAME":
return {
name: action.payload
};
case "RESET":
return {
name: payload
};
default:
return state;
}
};
const store = createStore(nameReducer);
//Dispatch action
store.dispatch({type: 'UPDATE_LASTNAME', payload:'Ganihu'})
store.dispatch({type: 'UPDATE_FIRSTNAME', payload:'Jude'})
console.log(store.getState())
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Try this out and take a look at the console.
Hey look at that! The name is changing. This is just a simple store, i don't think it is too complicated. We just have to create our store, then create our reducer and an action that will be dispatched to update our state.
Where does React come in?
Until now we have not done any react stuff, we have been doing strictly redux. Now its time to take a step further and see how we can dispatch actions and be able to update state from our react app. But before then, we will do a little clean-up. Create a new folder in your root directory, call it "redux". The redux folder will contain different files relating to redux, and we will start with the store.js file:
Store.js
import { createStore, combineReducers } from "redux";
import nameReducer from "./reducers/nameReducers";
const reducer = combineReducers({name: nameReducer});
const initialState = {
name: { name: "Kpose" }
};
const store = createStore(reducer, initialState,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__());
export default store;
Then we have the Actions and Reducers folders, which contains the action and reducer files respectively:
//nameActions.js
const update_firstname = {
type: "UPDATE_FIRSTNAME",
payload: "Jude"
};
const update_lastname = {
type: "UPDATE_LASTNAME",
payload: "Ganihu"
};
const reset = {
type: "RESET",
payload: "Kpose"
};
export {update_firstname, update_lastname, reset };
//name reducer
const nameReducer = (state = {}, {type, payload}) => {
switch (type) {
case "UPDATE_LASTNAME":
return {
name: payload
};
case "UPDATE_FIRSTNAME":
return {
name: payload
};
case "RESET":
return {
name: payload
};
default:
return state;
}
};
export default nameReducer;
//index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
//import store
import store from "./redux/store";
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
It is important to take note of combineReducers we encountered in store and understand why its there. Reducers know how to update the state by looking at the type of action coming in. The way reducers implement this logic is by using conditional statements. One popular way is to use a switch statement. In this case, our reducer manages changes to the application name, so it makes sense that one single reducer would handle all cases related to the name.But, what if we have unrelated aspects of our state to handle. What if our application had separate functionalities, like a user fetch, and a user update functionality?
Even though both these two functionalities deal with users, they have different responsibilities and, for the sake of making our application simpler and easier to maintain, it would be better to have separate reducers that handle these two functionalities separately.
We could name our reducers nameReducer and userReducer and put them into separate files inside the reducers directory.
But when we create a new Redux store with the createStore function, we can only pass one reducer to it. How are we supposed to fit two or more reducers as an argument to our function?
It turns out that Redux lets us combine multiple reducers into one that can be passed into createStore by using a helper function named combineReducers. The way we combine reducers is simple, we create one file per reducer in the reducers directory. We also create a file called store.js
In the store.js file we import the combineReducers function from Redux and we also import all the individual reducer files.
We then invoke combineReducers and pass to it as an argument an object that contains all the individual reducers. combineReducers will combine all the reducers passed to it into a single reducing function that can then be exported as default. We don't have multiple reducers yet, but we will soon when we expand our application so i found it important to prepare you for any surprises.
At this point we have a lovely little store with a reducer that knows how to update the state when it receives an action. Now it’s time to hook up Redux to React.
To do that, the react-redux library we installed earlier comes with 2 things: a component called Provider, and a function called connect. By wrapping the entire app with the Provider component, every component in the app tree will be able to access the Redux store if it wants to.
In index.js, import the Provider and wrap the contents of App with it. Pass the store as a prop.
//index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import store from "./redux/store";
ReactDOM.render(
<Provider store={store}>
<App /></Provider>,
document.getElementById('root'));
After this, our App, and all children of App – all of them can now access the Redux store. But not automatically. We’ll need to use the connect function on our components to access the store.
Next we will create a react component where we will be able to see result of dispatched actions and see the state updated real time. Lets create a name.js file then import them into our App.
//Name.js
import React from "react";
function Name (props) {
return (
<div>
<h2>Update Name</h2>
<div>
<button>Update First Name</button>
<br/>
<br/>
<span
style={{color:'blue'}}
> NAME IS: </span>
Name
<br/>
<br/>
<button>Update Last Name</button>
<br/>
<button>RESET</button>
</div>
</div>
);
}
export default Name;
//App.js
import React from "react";
import "./styles.css";
import Name from './Name'
export default function App() {
return (
<div className="App">
<h1>Redux Crash course</h1>
<div className="App">
<Name />
</div>
</div>
);
}
The buttons don't do much at the moment, lets change that. For us to be able to update our state when we press the buttons, we’ll need to use the connect function on our components to access the store.
import { connect } from 'react-redux';
Then we need to connect the Name component to Redux at the bottom:
//Name.js
import React from "react";
import { connect } from "react-redux";
import {update_firstname, update_lastname, reset} from './redux/actions/nameActions';
function Name (props) {
return (
<div>
<h2>Update Name</h2>
<div>
<button onClick={props.update_firstname}>Update First Name</button>
<br/>
<br/>
<span
style={{color:'blue'}}
> NAME IS: </span>
{props.name.name}
<br/>
<br/>
<button onClick={props.update_lastname}>Update Last Name</button>
<br/>
<button onClick={props.reset}>RESET</button>
</div>
</div>
);
}
const MapStateToProps = (state) => {
return {
name: state.name
};
};
const MapDispatchToProps = (dispatch) => {
return {
update_firstname: ()=> dispatch(update_firstname),
update_lastname: ()=> dispatch(update_lastname),
reset: ()=> dispatch(reset),
}
};
export default connect(MapStateToProps, MapDispatchToProps)(Name);
More on connect
Now that we have provided the redux store to our application, we can now connect our components to it. We established previously that there is no way to directly interact with the store. We can either retrieve data by obtaining its current state, or change its state by dispatching an action. This is precisely what connect does. Consider our code above, which uses connect to map the stores state and dispatch to the props of a component.
mapStateToProps and mapDispatchToProps are both pure functions that are provided the stores state and dispatch respectively. Furthermore, both functions have to return an object, whose keys will then be passed on as the props of the component they are connected to.
In this case, mapStateToProps returns an object with only one key : “name”, and mapDispatchToProps returns an object with the update_firstname and update_lastname keys.
The connect*ed component (which is exported) provides *name, update_firstname and update_lastname as props to Name component.
Phew! OK, now we should have our entire Redux cycle completely hooked up and running, and our app updates the state to our new "name" selection. Yes, yes, I know… it’s not the most styled up app of all time, but considering the boilerplate setup for Redux, let’s just stick to the concepts and feel free to style this up as you would like. But for now, pat yourself on the back and have a sip of coffee. Great work.
Up until now, we have been dealing with synchronous actions, our next challenge will be implementing an async action API fetch for user data information, we will be using data from REQ | RES API. If you go to the website, you can see that we can get a request for user data from
/api/users?page=2
. Ready?
What is Redux Thunk
Since reducers are supposed to be “pure,” we can’t do any API calls or dispatch actions from inside a reducer. We also can’t do that stuff inside a plain action creator! But what if we could make an action creator return a function that could do our work? Something like this:
function fetchUsers() {
return function() {
return fetch('/current_user');
};
}
Unfortunately, redux does not support actions like this, it only accepts plain objects as actions.
This is where redux-thunk comes in. It is a middleware for Redux, that enables Redux to deal with actions like fetchUsers(), above. You can dispatch these “thunk actions” like any other action creator: dispatch(fetchUser()).
A thunk is a name for a function that’s returned by another function. In Redux terms, the function being returned is the thunk, and the one that returns it is the action creator
The function you return from your action creator will be passed 2 arguments: the dispatch function, and getState. Most of the time you’ll only need dispatch, but sometimes you want to do something conditionally, based on some value in the Redux state. In that case, call fetchState() and you’ll have the entire state to read as needed.
Fetching Data with Redux Thunk
First, lets add redux-thunk as a dependency. Then, in our store.js, import redux-thunk and apply it to the store with Redux’s applyMiddleware function. Just make sure to wrap thunk in the applyMiddlware call or it won’t work. Don’t pass thunk directly:
//store
import { createStore, combineReducers, applyMiddleware } from "redux";
import nameReducer from "./reducers/nameReducers";
import thunk from 'redux-thunk';
const reducer = combineReducers({name: nameReducer});
const initialState = {
name: { name: "Kpose" }
};
const store = createStore(reducer, initialState, applyMiddleware(thunk),
);
export default store;
Next, let’s go back to our actions folder and write out our fetch API code in a new file, called fetchUsers.js, and also have a userReducer.js file in our reducer folder.
//fetchUsers.js
const fetchUsers = dispatch => {
fetch("https://reqres.in/api/users")
.then(res => res.json())
.then(res => dispatch({ type: "FETCH_USERS", payload: res.data }));
};
export default fetchUsers;
//userReducer.js
const userReducer = (state = [], { type, payload }) => {
switch (type) {
case "FETCH_USERS":
return payload;
default:
return state;
}
};
export default userReducer;
Back in our store, we can now import userReducer, add it to our combineUsers, set it to users, and set up its initial state, which will be an array. We also import Thunk and applyMiddleWare so that we can perform an async action using custom middleware. Edit our store to look like below, also notice our combineReducers in all its glory here taking our nameReducers and userReducer as arguments:
//store.js
import { createStore, combineReducers, applyMiddleware } from "redux";
import nameReducer from "./reducers/nameReducers";
import userReducer from "./reducers/userReducer";
import thunk from "redux-thunk";
const middleware = [thunk];
const reducer = combineReducers({ name: nameReducer, users: userReducer });
const initialState = {
name: {
users: [],
name: "Kpose"
}
};
const store = createStore(
reducer,
initialState,
applyMiddleware(...middleware)
);
export default store;
Alright! Now, let’s go back into our Name.js file and import our fetchUsers action. We can use the map method to map through our users and display the data by dispatch*ing our fetch with a button. Our *Name.js now looks like:
//Name.js
import React from "react";
import { connect } from "react-redux";
import {
update_firstname,
update_lastname,
reset
} from "./redux/actions/nameActions";
import fetchUsers from "./redux/actions/fetchUsers";
function Name(props) {
return (
<div>
<h2>Update Name</h2>
<div>
<button onClick={props.update_firstname}>Update First Name</button>
<br /> <br />
<span style={{ color: "blue" }}> NAME IS: </span>
{props.name.name}
<br /> <br />
<button onClick={props.update_lastname}>Update Last Name</button>
<br />
<button onClick={props.reset}>RESET</button>
<br /> <br />
<h2>Fetch Users</h2>
<button onClick={props.fetchUsers}>FETCH USERS</button>
{props.users.length === 0 ? (
<p> No users yet </p>
) : (
props.users.map(user => (
<p key={user.id}>
{" "}
{user.id} - {user.first_name} - {user.email}
</p>
))
)}
<br />
</div>
</div>
);
}
const MapStateToProps = state => {
return {
name: state.name,
users: state.users
};
};
const MapDispatchToProps = dispatch => {
return {
update_firstname: () => dispatch(update_firstname),
update_lastname: () => dispatch(update_lastname),
reset: () => dispatch(reset),
fetchUsers: () => dispatch(fetchUsers)
};
};
export default connect(
MapStateToProps,
MapDispatchToProps
)(Name);
Awesome! Now, if we check back in our app and we click on our button to fetch users, we should see our users being fetched, our first and last names are also updated from state.
You can check out what we have built so far on the codesandbox playground here https://codesandbox.io/s/clever-moser-qt5df
Conclusion
I realize that there is a bit of set up involved here, but once we get the wheels in motion and we set things up in a way where we can observe our data management and have a store and see how our components are wrapped and listen to each other, the fundamentals for Redux start to make much more sense. This is a great starting point.
This is just a starting example for Redux and, hopefully, now that we’ve gone over some of the core concepts and seen them in action, this can help clarify a few things and start off a good launching pad to further our knowledge.
If you have any questions about the concepts or code, please leave me a message, a comment or can also reach out to me on Twitter - @kpoosee and I’ll get back to you. Cheers
Top comments (0)