Meks (they/them)
Posted on December 19, 2020
Just a few days ago a passed my assessment for Flatiron's React.js/Redux project! π₯³ I've been over the moon excited and exhausted from all the studying prep and adrenaline I've been carrying in me. What felt like the bulk of the knowledge portion of the exam was spent discussing Redux. Here are my notes on what I learned through prepping for the assessment and discussing it with my assessor.
When it comes to talking about Redux, there's quite a bit of terminology involved and it's helpful to set some base definitions so we have the vocabulary to talk about it.
ACTIONS - A plain JavaScript object that has a type field. It is kind of like an event that describes something that happened in the application. An action object can have other fields with additional information about what happened. Conventions says to give that info a key of payload, but it's not strictly necessary.
REDUCERS - A function that receives the current state and an action object, decides how to update the state if necessary, and returns the new state. It's kind of like an event listener which handles events based on the received action (event) type. A typical flow for a reducer is this:
- Check to see if the reducer cares about this action
- If so, make a copy of the state, update the copy with new values based on the action's type and payload, and return it
- Otherwise, return the existing state unchanged
STORE - An object where the current state of the Redux application lives. The store is created by passing in a reducer.
DISPATCH - A Redux store method that is the only way to trigger state changes by passing in an action object.
The store will run its reducer function and save the new state value inside.
ACTION CREATOR - A function that creates and returns an action object. Most often used so we don't have to write the action object by hand every time we want to use it.
Ok, let's keep these in mind as we talk about the set up of Redux, and we'll go into more detail how they work and what they do as we go along.
What is Redux, what is it good for and why do we care?
Redux is a package that acts as a state management tool which allows the entire state of an application to be stored in one central location. In the context of React.js, one big advantage to this is that it helps avoid prop drilling. Each component of the app can have direct access to the state without having to send props down to child components or using callback functions to send data back up to a parent. To use it though does require a bit of set up.
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import { Provider } from 'react-redux'
import rootReducer from './reducers'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)))
ReactDOM.render(
<React.StrictMode>
<Provider store={ store }>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
What connects our React app to our Redux store is the Provider, and a nice trick to remember this is by the fact that the Provider is imported from 'react-redux'. The Provider makes the Redux store available to any components nested inside it (if they are wrapped in the connect() function, more on that soon).
When we wrap the entirety of the app in the we give it props of the store. That store is created by the function createStore() which is imported from Redux. The first argument of createStore is a reducing function that returns an updated version of state based on what is the current state and an action it is given to change its state. It also takes in an optional argument for an enhancer which can be used to add third-party capabilities to the store such as middleware. In my case I used applyMiddleware with thunk (another package) which allows for dispatching asynchronous actions in addition to normal actions. My middleware is wrapped in the composeEnhancers function which also makes my redux dev tools accessible in the browser.
Phew. Ok. Now that we've got some setup, lets go back and talk about the rootReducer that is imported and used in creating my store.
src/reducers/index.js
import { currentUserReducer } from './currentUser'
import { sightingsReducer } from './sightings'
import { combineReducers } from 'redux'
const rootReducer = combineReducers({
currentUser: currentUserReducer,
sightings: sightingsReducer
})
export default rootReducer
My rootReducer takes advantage of the helper function combineReducers() which takes in an object with the keys and values of different reducing functions. This then turns the different reducers into a single reducing function that is passed into createStore(). This is very useful for keeping the reducers organised and separating out concerns as an application gets bigger. The keys that are chosen here are the highest most level of keys in the state of my redux store. This is how I will gain access to pieces of the state and make changes to the state later on.
Now let's get into some of the nitty-gritty for how this is all working together. And we'll flesh out those terms I threw in up above such as connect, actions and dispatch.
For me the easiest way to understand is following the flow of the data. So let's look at the example of my app getting all of the nature sightings upon componentDidMount().
src/containers/SightingContainer.js
import React from 'react'
import { connect } from 'react-redux'
import { getSightings } from '../actions/sightings'
import Sighting from '../components/Sighting'
class SightingContainer extends React.Component {
componentDidMount(){
this.props.getAllSightings()
}
renderAllSightings = () => {
return (
<>
<h2 className='heading-secondary'>All Sightings</h2>
<section className="cards">
{this.props.sightings && this.props.sightings.map(sighting => <Sighting key={sighting.id} {...sighting} />)}
</section>
</>
)
}
render(){
return (
<>
{ this.renderAllSightings() }
</>
)
}
}
const mapStateToProps = state => {
return {
sightings: state.sightings,
}
}
const mapDispatchToProps = dispatch => {
return {
getAllSightings: () => dispatch(getSightings())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SightingContainer)
Remember when I said by wrapping the entire app with the and by giving it props of store, all of the app's components can have access to that store? They only have access if they are wrapped in connect(). Connect is a higher order component that has access to state and dispatch. Since regular React components do not automatically have access to them, connect allows them to interact with the store by wrapping the React component in a new connected component class.
connect() here takes in two arguments, mapStateToProps and mapDispatchToProps. We will come back to mapping state. mapDispatchToProps is function that is passed into connect, it takes in dispatch as an argument, and it defines what action you want and how it gets called in props of the component. It lets you create functions that dispatch when called and those functions are passed as props to your component.
The mapDispatchToProps function will be called with dispatch as the first argument. You will normally make use of this by returning new functions that call dispatch() inside themselves, and either pass in a plain action object directly or pass in the result of an action creator. In my case I pass in an action creator, which I imported at the top of my component. When using action creators inside dispatch it is a convention to simply name the field key the same name as the action creator.
const mapDispatchToProps = dispatch => {
return {
getAllSightings: () => dispatch(getSightings())
}
}
Since this is the case, an alternative to using mapDispatch to props is to pass the actionCreators directly into connect and destructure them.
export default connect(mapStateToProps, { getSightings })(SightingContainer))
Both ways of connecting my action creator to the component then give me access to the function getSightings through props. Now I can call getSightings() in my componentDidMount().
componentDidMount(){
this.props.getSightings()
}
This then invokes my action creator function in my sightings file in my action creators folder.
src/actions/sightings.js
const URL = 'http://localhost:3000/api/v1/sightings'
export const getSightings = () => {
return (dispatch) => {
fetch(URL)
.then(resp => resp.json())
.then(sightings => dispatch({
type: 'GET_SIGHTINGS',
payload: sightings
}))
}
}
Remember Thunk? This is where it is used! By itself, the Redux store doesn't know anything about async logic. It only knows how to synchronously dispatch actions, update the state by calling the root reducer function, and notify the UI that something has changed. Any asynchronous functions have to happen outside the context of the store. To do this we added the Redux thunk middleware which allows us to write functions that get dispatch as an argument. The thunk functions can have any async logic we want inside, and that logic can dispatch actions and read the store state as needed.
Here we can see that I have an anonymous arrow function that takes in dispatch as an argument, and it is making my async request to my backend. Once the promise is resolved I can dispatch a real action object to the reducer. Which is exactly what I do with the sighting object which will hold an array of all the sightings stored on the server.
So now we go off to the reducer!
src/reducers/sightings.js
export function sightingsReducer(state = [], action) {
switch(action.type){
case 'GET_SIGHTINGS':
return {...state, sightings: action.payload}
case 'ADD_SIGHTING':
return {
...state,
sightings: [...state.sightings, action.payload]
}
default:
return state
}
}
The reducer takes in two arguments, the first is the current state of the store, and we give it a default value of some kind, whether that is an empty array, empty hash, null, or something else of our choosing. Since sightings will be an array, I default to an empty array. This means that in my Redux store I have:
sightings: []
Remember the root reducer and the keys set up there? Yup, that is where the sightings key is coming from.
The second argument passed to the reducer is the action object that was dispatched from the action creator. The switch case checks the action.type and goes to the case of 'GET_SIGHTINGS' since that is what is in the action object.
Once matched up with the proper case, the reducer will perform changes to the state using the payload passed in with the action and the instructions in the return statement. Since we don't want to mutate state directly, we make a copy of state using the spread operator and set the sightings key within state to the value of action.payload, which remember is the array of sighting objects that was fetched from the backend. At this point if we check out the store using our dev tools, it looks like this:
sightings: [{id: 1, commonName: "Bald Eagle"}, {id: 2, commonName: "Great Blue Heron"}, {id: 3, commonName: "Red Tailed Fox"}]
We now have a populated array of sighting objects!
Next we want to access that state and use it to render the data to the DOM. Let's go back to our SightingContainer component and check out mapStateToProps.
const mapStateToProps = state => {
return {
sightings: state.sightings,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SightingContainer)
Just like how we had to give the component access to dispatch through connect, we also have to do the same with the state of the store. mapStateToProps is a function that is passed in as the first argument to connect, and this function takes in the entire state of the store as an argument. It returns an object where you get to decide data you want to get from your store and how you want it to be called. We can set a key of sightings which will give us this.props.sightings with a value of state.sightings which will return us the sightings array that is in the store. Recall that sightings key of state.sightings is coming from the root reducer where we set sightings: sightingsReducer.
Now that we have access to the array we can then use our React tools to iterate through them and render a new component for each sighting. Just like we would if we'd gotten the array of sightings from a parent.
renderAllSightings = () => {
return (
<>
<h2 className='heading-secondary'>All Sightings</h2>
<section className="cards">
{this.props.sightings && this.props.sightings.map(sighting => <Sighting key={sighting.id} {...sighting} />)}
</section>
</>
)
}
render(){
return (
<>
{ this.renderAllSightings() }
</>
)
}
Those are the key points that were covered in my assessment with some extra info and links to resources for more information. The Redux documentation is very comprehensive and there are even more examples and detailed explanations than I was able to cover here.
To recap, Redux is a state management tool that we can use to store the state of an application in one central location. To connect React with Redux we wrap the app in a and give it an attribute of the store. connect() is used to give particular components access to the store. It takes in arguments of mapStateToProps and mapDispatchToProps which are functions that respectively take in state and dispatch as arguments. These can then be used within the component to get state and display it to the DOM (mapState) or to invoke action creators (mapDispatch) to change state. If we need to use asynchronous functions we create the store with a middleware such as Thunk so that a dispatch can return a function. Within such a function dispatch will also return its expected action object which is sent to the reducer with a type and a payload. Using that information the reducer will update the state of the store appropriately.
Thanks Redux for making my state organised and keeping my components much cleaner. And goodbye prop drilling!
I'm still amazed that I was able to share most of this information in a coherent way during an exam. Hopefully this may help someone else studying or learning Redux.
Happy Coding and Happy Studying!
Posted on December 19, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.