Skip to content

Instantly share code, notes, and snippets.

@Arrow7000
Created August 20, 2018 09:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Arrow7000/8ed726a27052beb4aee08394a85fe8bc to your computer and use it in GitHub Desktop.
Save Arrow7000/8ed726a27052beb4aee08394a85fe8bc to your computer and use it in GitHub Desktop.

Intro to Redux

In my first article for the FundApps blog, I'd like to write up a talk I gave a while ago, giving a quick introduction to Redux.

This article assumes you are familiar with modern JavaScript and React.

React Recap

Before diving into Redux let's quickly recap what React is.

React is a popular view library for the web. It is generally used with a new syntax called JSX, which stands for "JavaScript XML". JSX allows you to embed XML-like markup into your JavaScript code, making it a much more natural way of creating user interfaces.

React is also unidirectional. This means that – unlike for example with Angular – data can only flow from parent components to child components and not the other way round. Child components can only affect higher-level data or state if given a function that can do that by a parent component. They cannot change anything happening on a higher level.

See the example below.

https://gist.github.com/dafb308c9b8881b8fdf4b2f24af2af04

Here is a simple Counter component. Its state object contains a single count property that can be either incremented or decremented by the two eponymous methods.

In addition to displaying the current count, our Counter's render method also displays incrementer and decrementer buttons. As you can see we need to pass the increment/decrement methods to the button components from the same component that the state is contained in.

But – I hear you ask – why can't we just make the buttons change the state directly, by doing something like this:

https://gist.github.com/1f76e4cf5b76fa87dff630ac0019a20b

I.e. why can't we just change the count property on the props object?

It is a nice dream, but unfortunately that throws the nasty error: Cannot add property count, object is not extensible. The only way to change Counter's state is by using a method from Counter itself.

This is what unidirectionality means. The flow of data only goes down, never up. A child component can never change a parent's state. Although there are very good reasons for this, it can make it really difficult to change state in large React apps, when you have lots of deeply nested components. If you want a deeply nested button to change the top-level state you are going to have to pass the state-changing method from the top-level component all the way down to the lowly button, through potentially dozens of intermediate components – something known as "prop drilling".

This is where Redux comes in.

Redux: what is it

Redux is a solution to this problem. It is a way of managing state in React – and other view libraries – in a manageable and predictable way, without unnatural amounts of prop drilling.

Structure

Redux consists of a few different parts: actions, reducers, the store, and connected (or "smart") components.

  • Actions represent discrete, well, actions that change your application's state.
  • Reducers determine how your application state changes in response to each action.
  • The store actually contains your application state and your reducer(s), plus it makes your state available to connected components.
  • Connected components hook into the store and can dispatch actions to it

Actions

An action is an object that contains at least one property that specifies what kind of action it is. By convention this is usually made to be the type property. Action objects can also contain additional properties containing data relevant to the state change they will cause.

Action creators are functions that return objects, this is useful when you want to create actions that can contain arbitrary data, e.g. if you want to create an "INCREMENT_BY" action that increments the count by an arbitrary amount – in that case you need the action to contain the number specified in your action creator via a parameter.

https://gist.github.com/9d8da412d1ccfb2fe99bbfe82ae751aa

These actions will get passed to a reducer, which is what actually determines how your action will affect the application state.

Reducers

Reducers are functions that do one thing: given the current state and an action, they return the new state. They determine how your application changes in response to each action.

They usually contain just one switch statement on the action's type property with a case for every possible action type. In each case they then return the modified state – potentially extracting other data from the action in the process.

https://gist.github.com/dc66b254e36cf6009b4dec0ff79d66b3

For complex tree-like application states, it can make more sense to have separate reducers, each worrying about only part of the state tree. Redux allows for reducers to be composed in this way using the combineReducers function, which would look like so:

https://gist.github.com/59598ca817af0e94d216644700c17a40

Store

The store is what actually holds the application state. It is constructed by taking the reducer plus some optional middleware (more about which later). It is then passed into react-redux 's Provider component, which makes it available to all descendant components via the [React Context API](link to RCA).

Dispatching an action to the store is what makes its state change. You can trigger an action to be dispatched manually by calling store.dispatch(action) – although in practice you will never need to do this because you will only dispatch actions from inside your connected components. More on this later on.

https://gist.github.com/0ec7de769b58a44be99129f638995bab

Connected React components (via React Context API)

The above is all very nice and abstract, but what does it actually mean for you trying to build a React app? Well this is where connected or "smart" components come in.

Redux provides wrappers that can wrap a dumb function and hook it up to the store, allowing it to both get state data and dispatch actions. This is the connect function. It is generally used as in the snippet below.

https://gist.github.com/5c0c47cdba74f350af30fffda75ecc27

Let's explain what's going on here.

First you have Counter, which is a simple component that knows nothing about the outside world. All it knows is that it takes 3 props: count, which is the current count to display; and increment and decrement, which are functions that should respectively increment and decrement the count. However these are just props provided to it by its parent. The component doesn't know or care how they are implemented.

Then there is mapStateToProps. This is a function that takes the current store's state and maps it to props that the component expects to receive. In our case all it retrieves from the state is its count property, which it passes on to its child component as displayCount. However this could include more serious transformation logic, like state => ({ displayCount: state.count, squareOfCount: state.count ** 2 }) for example.

Finally, mapDispatchToProps is a function that maps the component's functional props to action creators. It takes one parameter, dispatch, which should get called with an action every time you want to, well, dispatch an action. You can pass parameters by just adding them in like so incrementBy: count => dispatch(incrementByAC(count)).

The result of calling connect(mapStateToProps, mapDispatchToProps) is a smart component that can wrap a dumb component like Counter. This will connect it to the store and action creators in exactly the way you've specified.

Structure summarised

To summarise:

  • Actions are objects – containing at least a type property – that represent atomic changes in the state of your application
  • Reducers are functions that take in an action and the current state of your application and return the new state
  • The store is where the core of your application lives, it is constructed by taking in your reducer and any middleware you may have (see next section)
  • Connected components are where you can actually use your application state's properties in your React application, as well as trigger state changes through dispatching actions

Middleware

You can also add middleware to Redux. These are functions that can do something with an action before it reaches the reducer. A commonly used one is Redux Thunk, used to trigger actions when an asynchronous function completes. Another handy one is the Redux DevTools extension, which allows you to view and manipulate your Redux store from a browser extension; it will make debugging your Redux setup much easier.

Why use it

Hopefully by this point you have some understanding of what Redux is and how to use it. Seeing how it works may also already have given you an idea of why you should use it. But in case you haven't, let's summarise them here.

  • Redux let's you properly manage state in React apps without having to engage in prop drilling.
  • It gives you highly predictable – and easily testable – state transitions.
  • When using the Redux DevTools extension, you can use the time-travelling debugger to easily debug your state change logic.

Nothing is without negatives however, and Redux is no exception. In my opinion these are:

  • There is a lot of boilerplate you have to write before you can get started. For example even for a really simple app you need to set up your store, reducer, actions and connected components – which themselves include state and dispatch mappings.
  • It tends to be quite verbose, with a lot of code to write in several different places to add functionality.
  • Because the type and contents of your action is tightly coupled to your reducer and connected components, making even a small change means updating a whole bunch of code in several different places. Without a good typing system, like TypeScript for example, it can be really difficult to keep track of what needs to be updated in response to a small change in one place.

In conclusion

Hopefully this post has given you enough for you to be able to get started building your first Redux app. Have fun doing so and don't forget you can always read the docs when you get stuck!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment