Redux is a popular state management library for React applications. It enforces a strict unidirectional data flow and provides a central store for your application’s state.
Redux is a predictable state container for JavaScript apps
Core Concepts
Redux is based on three core principles:
- Store: A single, immutable source of truth for your application’s state.
- Actions: Plain JavaScript objects that describe a change in the application’s state.
- Reducers: Functions that specify how the state changes in response to actions.
Redux API
Redux, for all of its power, has a relatively small API footprint, it is really small and has few things attached to it:
- ➡️ Apply Middleware
- ➡️ Compose
- ➡️ Combine Reducers
- ➡️ Bind action creator
- ➡️ Create Store
- dispatch: ƒ dispatch(action)
- getState: ƒ getState()
- replaceReducer: ƒ replaceReducer(nextReducer)
- subscribe: ƒ subscribe(listener)
Yup, just five functions. We'll discuss each of this in due time. We'll start with one of the simple utility methods that come along with Redux.
A reducer is simply a function that takes in a state, and the things that happened and only one thing comes out of it, a new state
Compose
Functional programing concept!😍
The compose
function takes multiple functions as arguments and returns a new function that represents the composition of these functions. In Redux, it’s commonly used to apply multiple store enhancers or middleware to the store creation process.
Info
compose()
isn't exactly Redux-specific. It's just a helper function.compose
takes a series of functions as arguments and returns a new function that applies each those functions from right-to-left
Let’s say that we had a bunch of functions that each take a string and return a modified string.
(Yea, I just learned that some of those methods exist too.)
We could call them all like this:
But, what if I wanted to pass this combined function around as an argument to another function or method? I’d have to do something like this:
This is tedious.
compose
gives us a simple way to compose functions out of other functions.
This is used as a helper when creating enhancers, which we’ll talk about in the a more advanced workshop. For now, I’m just fulfilling my promises of demystifying the core API.
Create Store
edux is a friendly state management library. In this industry, we tend to call the place where we keep our state the “store.” Redux’s createStore()
method will … create … a … store
One does not just create a Redux store, however.
This will blow up, but at least we get a helpful error message.
Error: Expected the reducer to be a function.
- So, there are two things to take away from this:
- We need to provide an argument that is a reducer and a reducer is a function.
- A function that takes a reducer and creates you main state returns you functions to interact with your store
Reducer
A reducer is a function where the first argument is the current state of the world and the second is something that happened. Somewhere inside of the function, we figure out what the new state of the world ought to be based on whatever happened.
That's the job of the reducer. It looks at the new thing that happened and it looks at the current state of the world and returns a new state of the world.
Here’s the crazy thing: it’s just a function. It takes two arguments: the thing that just happened and the current (soon to be previous) state of the world. It returns one thing: the new state of the world.
An action is just an object. The only requirement is that is has a type
property. Sure, something happened, but what type of thing happened?
A Word on Conventions Around Action Types
There are a few different patterns for naming your action types. For a long time, it was convention to use
SCREAMING_SNAKE_CASE
. As we’ll see in a little bit, we frequently alias our action type names to constants in JavaScript, we we can’t have spaces. UsingSCREAMING_SNAKE_CASE
also helps separates our constant action type names from other variables in our application.
Updating State Based on Actions
Let’s say an increment action comes in. Well, we should probably increment the value, right?
Alright, there are a few things here. You’ll notice that we’re creating a new object rather than mutating the existing one. This is helpful because it allows anything depending on this state to figure out that we have a new state of the world. We also want to make sure we return the existing state in the event that an action we don’t care about comes through the reducer.
We can make the above a little bit better too!
You’ll notice we made a constant called
INCREMENT
. This is what I was talking about before. The main reason that we took this approach is because we needed to make sure that we didn’t accidentally misspell the action type—either when we created the action or in the reducer. At least now, our code will blow up. This sure beats silently failing.
We’ll also typically use functions to create our actions since they might need more information.
Calling it a payload
is also just a convention. You’ll probably notice that nothing in the last few code samples have literally nothing to do with Redux. It’s all just JavaScript.
We now have an initial state of the world and some logic about how it should change in the event that a limited set of things happen.
Store API
Alright, so we made a store. Redux stores also have a relatively small API surface area. one store for the entire application
- 💡 It has the following responsibilities
- ➡️ Holds application state
- ➡️ Allows access to state via getState()
- ➡️ Allow sate to be updated via Dispatch(action)
- ➡️ Register listeners via subscribe(listener )
- ➡️ Handle unregistering of listeners via the function returned by subscribe(listener)
Redux Store Methods
replaceReducer
Takes one reducer and replace it with another one
The Redux store exposes a replaceReducer
function, which replaces the current active root reducer function with a new root reducer function. Calling it will swap the internal reducer function reference, and dispatch an action to help any newly-added slice reducers initialize themselves:
getState()
This simply gets a state
dispatch: ƒ dispatch(action)
Sends actions into the reducer to do mutations to the current state of the application
We made some initial state. We made the outlined some things that could happen to that state. We even defined what should happen to the state when those actions happen.
So, how do we get actions into that reducer to modify the state? Well, we dispatch them.
Our action creators are functions that return actions, so might see something like this:
Once an action is dispatched, the reducer takes care of the rest.
The system works. You probably understand most of Redux at this point. Seriously.
subscribe: ƒ subscribe(listener)
Subscribes to the store and when something changes, then do domething!
If we think about this in terms of React, we’re probably not expecting that our components call store.getState()
all the time. Rather, we would think that our store would know that its state has changed and pass different props to the components.
That’s a good thought acutally. In the last section, we noticed that the store has a subscribe
method. This method takes a function dictates what should happen whenever the state in the store is updated.
subscribe
also returns a function that you can call to unsubscribe.
- getState: ƒ getState()
- get the state of our store
- replaceReducer: ƒ replaceReducer(nextReducer)
- Swaps out the reducer
Bind action creator
Let’s get back to Redux itself for a moment. So far, we’ve made functions that create actions (a.k.a. action creators) and we’ve passed them to dispatch
.
Well, what if we wanted to wrap that whole process into one nice neat package.
This could be tedious. What else could we do? We could use compose
.
And if we wanted to a whole bunch, we could get fancy.
But, like, React likes its props in objects and then we’d have to get all fussy with Object.keys
or Object.entries
. I’m very lazy. We should just have an helper for that. Oh, wait, we do.
There is no rule saying that you have to use bindActionCreators
. It’s there to help you.
Binds an action, then returns a new object with the same keys, but each action creator is wrapped in a dispatch
call. This makes it easier to invoke the action creators directly, and the resulting functions automatically dispatch the actions.
Combine Reducers
Let’s stay we have an application where we have some users and we have some tasks. We can assign users and we can assign tasks.
The state might look something like this.
This is still way smaller than a real application, but it’s still becoming a pain. Let’s look at what the reducer might look like.
The kind of monolithic reducer you would have.
This is already getting out of control. It would be nice if we could split out the idea of managing our users and managing our tasks into two separate reducers and just sew everything back up later.
It turns out, we can! We just need to use
combineReducers
.
Fun Fact!: All actions flow through all of the reducers. So, if you want to update two pieces of state with the same action, you totally can.
Actions
Synchronous Actions
Actions are plain JavaScript objects that describe events or user interactions in your application. They represent what happened and the data associated with the event. An action object typically includes two properties:
-
type
:- ➡️ A string that describes the type of action. It’s a unique identifier for the action.
-
payload
:- ➡️ An optional property that carries data related to the action. This can be any valid JavaScript value like an object, string, or number.
Here’s an example of an action object:
Actions serve as a way to communicate changes in your application, and they are dispatched to the Redux store, where reducers respond to them
Async Actions
Asynchronous API calls to fetch data from an endpoint and use the data in your application
Reducers
specify how the app’s state changes in response to actions sent to the store function that accepts state and action as arguments and returns the next state of the application
(previousState,action) => newState
Hook Redux in react 💗
We’re starting from base principles. So, we’re going to have to do this from the ground up. Where to begin?
Well, an interesting thing to think about is the basic state and the actions that can occur in a given feature—or, an entire application in this case.
We have…
- ✅ A count.
- ✅ The ability to increment that count.
- ✅ The ability to decrement that count.
- ✅ The ability to set that count to zero.
If we’re being intellectually honest, we could handle this in a bunch of ways.
- 💡 We could make actions for these three specific use cases (e.g.
INCREMENT
,DECREMENT
, andSET
). - 💡 We could make a super generalized called
SET
where we set it a given number every time. - 💡 We could split the difference and have
INCREMENT
andDECREMENT
and the have an action type that handles the edge cases.
Info A general rule: Your actions should say what happened and not really have any opinions about what that means. Let your reducers figure that all out on your behalf.
I like the first option. So, what would that look like?
We can make a file called
actions.js
.
That seems fine. What would a
reducer.js
look like?
Okay, so we’ve looked at Redux and we’re working with some assumption that you’ve seen React before. But, it turns out that there is a bit of a layer in between and it’s a library called React Redux.
Luckily, it’s another fairly small library as well.
The first and arguably most important part—is a component called
Provider
. This is a component that takes the store and threads it through your React application using the Context API.
First, let’s go ahead and create our store in
store.js
.
Now, in index.js
, we’ll hook it up to React.
With that—all of your components now have access to the Redux store—from a certain point of view.
Adding the Redux Dev Tools
First and foremost, install the Redux Developer Tools in your browser if you haven’t already.
We’re not totally there yet, we still need to hook our application up to the tools.
The extension adds some properties onto the global window
object. One of these is an enhancer that we can use.
We can pass this to our store when we create it in store.js
.
Now, we have some insight into the current state of the store and the actions that are firing through the application.
Hooking Up State
Basics of useSelector
useSelector
is a hook provided by the React-Redux library. Its primary purpose is to extract data from the Redux store in a React component. As you delve into web development and React in particular, mastering this hook will significantly enhance your state management capabilities.
Alright, so we have Redux connected to React in spirit, but the component isn’t really using anything here.
Key Features
- ✅ Selector Function
The first argument of useSelector
is a selector function. This function takes the entire Redux state as its argument and returns the specific data from the state that the component needs. It allows you to precisely pinpoint the slice of the store that is relevant to your component.
- ✅ Reactivity
One of the powerful aspects of useSelector
is its reactivity. When the selected part of the Redux store changes, the component will automatically re-render. This simplifies the process of managing and updating the UI based on changes in the application state.
- ✅ Memoization
Under the hood, useSelector
uses a memoization technique to ensure that the selector function is only re-invoked when the relevant parts of the state tree have changed. This optimization enhances performance by preventing unnecessary renders.
Best Practices and Gotchas
- ✅ Keep Selectors Simple
While the selector function can be complex, it’s generally a good practice to keep it as simple as possible. Complex selectors can lead to performance issues, as they might get re-invoked more frequently than necessary.
- ✅ Reselect for Memoized Selectors
For larger applications or scenarios where performance is critical, consider using the Reselect library to create memoized selectors. This can significantly improve the performance of your selectors, especially if they involve complex calculations or transformations.
- ✅ Avoid Inline Arrow Functions
When using useSelector
, avoid defining inline arrow functions within the component’s render. This can lead to unnecessary re-renders, impacting performance. Instead, define your selector functions outside the component.
useSelector
We’re going to get more into selectors in a bit, but—at a high level—they do what they say they do on the tin: they select pieces from our state tree.
It’s zero by default in our state, so our UI hasn’t changed much.
Hooking Up Dispatch
Okay, asking our users to install the developer tools and dispatch actions by hands seems not great. Maybe we should get those buttons working?🤣
In order to do that, we’re going to need them to start dispatching some actions.
This is store.dispatch
from our store.
Let’s import our action creators.
And now we can dispatch those action creators.
Opinion on Using
subscribe
in ReactWhile
subscribe
offers a lower-level way to listen to store changes, I would recommend leveraging React-Redux hooks likeuseSelector
anduseEffect
in most cases. These hooks provide a more declarative and React-friendly approach to handling state changes within components. The use ofsubscribe
can be reserved for specific scenarios where fine-grained control over store changes is necessary. striking the right balance between advanced techniques and best practices will be key to building robust and maintainable React applications.
Binding Actions
We saw in our introduction that we can create functions that implicitly know about or are bound to—dispatch.
At a simple version of this might look as follows:
And now we could put them into the component.
You could even create a hook if you wanted to do this on the regular.
In a new file called set-actions.js
:
And now we can use this whenever.
Using the Connect API
…