I've got to admit, I was VERY confused about redux after our first class about it on Monday. I think part of it was because our guided project during lecture was also a simultaneous refactor of an application that was just using a reducer pattern. Regardless, I didn't understand what all of the little pieces were and what they were doing. So on Wednesday, I rewatched all of the pre-class preparation videos about redux before our next lecture. It was like someone flipped a switch in my head! Everything clicked and I was like, "Oh I totally get it now!" Such a great feeling :)
So here's what I've learned about redux! Redux is a state management library that is the "single source of truth" for your application level state. This state tree, as it's called, is held in what is known as the store
as a JavaScript object. This state is immutable, meaning we never directly change it. To update the state, we create a clone of the current state by destructuring it, do our logic to make necessary changes, and then replace the original state with the updated copy.
The way that we update our state is via a function called a reducer
. Reducers are pure functions, meaning that they return the same output as the input they were given. In this case, reducers receive the current state and return the new, updated state. The second parameter that a reducer
receives is called an action. This is how the reducer
knows what to do to the state. You can read more about it in my post from Week 21 of Lambda School.
Setting up React for Redux
In a React application, here's an example of how you might set up your App.js file to be able to use redux:
import React from "react"import { createStore } from "redux"import { Provider } from "react-redux"import { myReducer as reducer } from "./reducers/myReducer"const store = createStore(reducer)const App = () => {return (<Provider store={store}><h1>Hello world</h1></Provider>)}
I like to set up in App.js, but you can also do this in index.js and in the root element wrap the App
component in the Provider
instead.
The Provider
component takes a prop store
, which we give the value of the store
that we create via the createStore
function imported from react-redux. By wrapping our entire application in this Provider
, we can now give all of our components access to the state tree held in our store
.
In the above code snippet, there is also the assumption that we've created our reducer
function in a separate index.js file in a "reducers" directory, so it needs to be imported before passing it into our createStore
function.
Accessing the Store in Child Components
Now that we have our application level state in store
via our reducer
and our App
wrapped in Provider
, how do we access the state in our components? We can use the connect
function from react-redux. Let's take a look at what it looks like and then discuss it:
import React from "react"import { connect } from "react-redux"const MyComponent = props => {return (<div><h1>{props.myState}</h1></div>)}const mapStateToProps = state => {return {myState: state.myState,}}export default connect(mapStateToProps, {})(MyComponent)
Let's break this down a bit. We import connect
from "react-redux" and use it at the bottom of our component file when we do our "export default". Here, we actually invoke connect
twice. This is an example of function currying, or creating a function that returns another function. connect
takes 2 arguments: a function and an object.
The function is one that we create that we've called mapStateToProps
. The object returned by mapStateToProps
tells connect
which pieces of our application level state (held in store
) that we want to bring into this component. Then to use it in our component, we access it via props
.
The object passed into connect
is called an action. Simply put, an action
is an object with a key type
and an optional key payload
. The value of type
is a string that describes what action just occurred. By convention, the syntax for the string is all caps, with words separated with underscores. The value of payload
is the data that goes along with the interaction that the reducer
needs to be able to properly update the state. An action is returned by a function called an "action creator".
export const ACTION_TYPE_NAME_HERE = "ACTION_TYPE_NAME_HERE"export const myActionCreator = () => {return {type: ACTION_TYPE_NAME_HERE,payload: "A string of what I want to update state with",}}
We can create a variable assigned to the string value of our type
value so that we can reduce the number of bugs that occur from annoying misspellings. Then we can take advantage of an IDE's autofill/autosuggestion when we're using variables and import it into our reducer file.
Actions are "dispatched" to the reducer
, or passed into the reducer
function as an argument. Inside reducer
, we can switch
the case
to do different things to our state based on the value of type
that is received from the action.
import { ACTION_TYPE_NAME_HERE } from "../actions/myActions"export const myReducer = (state, action) => {switch (action.type) {case ACTION_TYPE_NAME_HERE:return {...state,myNewString: action.payload,}default:return state}}
This is all great and good, but how do we implement functions, or action creators, to actually update our state from our components? All we've done so far is provide a way to access the values stored in our application level state.
Going back to our connect
function, remember the empty object we passed in as a parameter? Inside of this object is where we pass in any action creators we need to use in that component.
import React from "react"import { connect } from "react-redux"import { myActionCreator } from "../actions/myAction"const MyComponent = props => {return (<div><h1>{props.myActionCreator()}</h1><p>Do something with myState here: {props.myState}</p></div>)}const mapStateToProps = state => {return {myState: state.myState,}}export default connect(mapStateToProps, { myActionCreator })(MyComponent)
This is an overly simplistic example, but it still demonstrates the layout for how to incorporate an action creator into your components. We import it at the top and then pass it into connect
. We then can access and invoke the action creator via props, just like with our state from mapStateToProps
. And that's it! You can create all sorts of action creators that allow you to update state in the reducer and always know exactly where your state is being changed. Redux does all of the "connecting" for you between the reducers, actions, and components.
Asynchronous Redux
By nature, redux is synchronous. But sometimes we need to perform asynchronous activity before redux "dispatches" our action over to our reducer and updates our state. A prime example would be when we need to make an API call and wait for the data to come back so that we can then pass that data to our reducer via our action payload
. We can add something called "middleware" to our application so that our data flows first to the action, then to the middleware, and finally to our reducer to be updated. A middleware that we learned at Lambda School this week is redux-thunk, created by Dan Abramov. This allows us to perform asynchronous operations inside of our action creators.
To be able to use redux-thunk in our react application, we need to add the middleware back where we first created our store
. Here's what our App.js structure now looks like:
import React from "react"import { createStore, applyMiddleware } from "redux"import { Provider } from "react-redux"import thunk from "redux-thunk"import { myReducer as reducer } from "../reducers/myReducer"const store = createStore(reducer, applyMiddleware(thunk))const App = () => {return (<Provider store={store}><h1>Hello World</h1></Provider>)}export default App
A "thunk" is simply a function that is return by another function. When our action creator is called, redux-thunk intercepts and acts on the returned data. If that returned data is an action, then the middleware forwards it onto the reducer. But if our action creator returns a function, then it invokes that function (now a thunk) and passes the dispatch
function to it as an argument. Let's look at an example:
export const USER_LOGGED_IN = "USER_LOGGED_IN"export const logInUser = credentials => {// this returned function is the thunk, and gets dispatch passed inreturn function (dispatch) {return axios.post("/login", credentials).then(res => {const loggedInAction = {type: USER_LOGGED_IN,payload: res.data.user,}dispatch(loggedInAction)})}}
We can clean this up a bit to be a little more readable by using an arrow function:
export const USER_LOGGED_IN = "USER_LOGGED_IN"export const USER_LOGIN_ERROR = "USER_LOGIN_ERROR"export const logInUser = credentials => dispatch => {return axios.post("/login", credentials).then(res => {const loggedInAction = {type: USER_LOGGED_IN,payload: res.data.user,}dispatch(loggedInAction)}).catch(err => dispatch({ type: USER_LOGIN_ERROR, payload: err }))}
The thunk has access to dispatch
, and can dispatch, or send, a new action to the reducer based on the result of the API call! This is what makes redux-thunk, and middleware in general, so powerful!
This week's project
To practice implementing redux, one of our projects this week was to use create-react-app and any free, public API that we wanted to create an application that uses redux for the state management. I LOVED this project because there was so much creative freedom. I created a Pokédex using the PokeApi! And while I borrowed the styling from Eric Varela, all of the logic for the different API requests and state management was done by yours truly. I'll post the link here when I deploy it, but for now, here's the github repo.
Update: Here is the deployed Pokédex app (best viewed on desktop).