Agusti Bau

Climbing the React state ladder

May 26, 2020

6 minutes

Intro

This is a blog post summarizing my learnings and choice of libraries in the React-Redux world. Specifically I will talk about why I choose redux-toolkit and redux-saga.

You’ve probably heard of React, it’s the trendy UI framework made by Facebook that everyone uses nowadays. It’s main goal is to make frontend web development scalable to teams and codebases of any size.

In order to make the codebase scalable, code needs to be predictable and easy to understand. Code changes in one part of the code should not ripple through all the codebase.

Traditional frontend codebases usually evolved into spagheti dishes. Global variables, heavy coupling between javascript and html files, event handlers everywhere… Luckily this is history now, (a lot of) tooling and ES6 have come a long way, and now frontend code is way more maintainable.

React has been a key contributor on making frontend code more maintainable, but also has increased the amount of tools and libraries required to understand. In this post I will explain some of the tools I’ve come across that make React code more maintainable.

I’m going to assume some knowledge of React, reading the Overview section of the official documentation should be enough.

Unidirectional data flow

The way which data is represented in the React ecosystem, is through properties and state. State is when data is held within the component and props is when data comes from a parent component. This is what is know as unidirectional data flow where data travels from parent components to child components. This restriction complicates some code that traditionally would have not been an issue.

function TodoList ({todos}) {
    return <ul>  
        {todos.map(t => <li key={t} >{t}</li>)}
    </ul>
}

function AddTodoView ({addTodo}) {
    const [newTodo, setNewTodo] = useState("")
    const onInputChange = e => {
        setNewTodo(e.currentTarget.value)
    }
    const _addTodo = () =>{
        addTodo(newTodo)
        setNewTodo("")
    }
    return <div>
        <input type="text" value={newTodo} onChange={onInputChange} />
        <button onClick={_addTodo}>Add</button>
    </div>
}

function TodoView () {
    const [todos, setNewTodos] = useState([])
    const addTodo = (newTodo) => {
       setNewTodos([...todos, newTodo]) 
    }
    return <div>
        <TodoList todos={todos}/>
        <AddTodoView addTodo={addTodo}/>
    </div>
}

function App() {
  return (
    <div className="App">
        <TodoView />
    </div>
  );
}

In this classic example we have a barebones TO-DO list. We are using react hooks, so the state is defined by the return of the useState calls. We are not going to talk about the state within the AddTodoView function. That is only there to handle the form data. The one that is interesting is the one inside the TodoView function. If you notice, the state is held in said component and then passed as a property to the TodoList component.

The problem I’m trying to explain comes when another components requires this state. Let’s say that now we need a header that should show the amount of todos we have.

function TodoView ({todos, addTodo}) {
    return <div>
        <TodoList todos={todos}/>
        <AddTodoView addTodo={addTodo}/>
    </div>
}

function Header({todos}) {
    return <header className="App-header">
        We have {todos.length} things to do!
      </header>
}

function App() {
    const [todos, setNewTodos] = useState([])
    const addTodo = (newTodo) => {
        setNewTodos([...todos, newTodo]) 
    }
    return (
        <div className="App">
            <Header todos={todos}/>
            <TodoView todos={todos} addTodo={addTodo}/>
        </div>
    );
}

As you can see now we have lifted the state up one level. As you can see now the state is in the new common parent component and flows down through properties. This becomes a problem as the state grows bigger or the hierarchy taller.

That’s why React is most commonly used with some kind of state management library.

State management frameworks

State management libraries provide piping in order to access some kind of global state.

There’s a few state management libraries out there, there’s MobX and the original Flux but the most popular one, and probably the one you have heard about, is Redux .

I used Redux as is the most mainstream one and I’m not even sure what things Redux will be lacking.

Redux

Redux adds a lot of new concepts, but it’s not too hard to wrap our heads around. I summarize it as follows: Redux state is defined in the store, changes are requested through actions, the reducer processes actions in series, altering the state.

Actions

Actions are plain objects that indentify a request for a state change. It is common practice to use redux standard actions which establishes a format that is meant to be used by middlewares (more on that later)

This is an example of a non-error based action.

const action = {
      type: 'ADD_TODO',
      payload: {
 	     text: 'Do something.'  
      }
}

It’s important to remember that actions are plain objects. They are dispatched through the dispatch method which can be obtained by the useDispatch hook.

import { useDispatch } from 'react-redux';
...
const dispatch = useDispatch()
dispatch(action)
...

Reducer

Reducers are functions that take an action as a parameter and change the global state accordingly. It is done in an inmutable way (we don’t mutate the state variable, but we return a new object with the new state)

function reducer(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      const newTodos = [...state.todos, action.text]
      return Object.assign({}, state, {
        todos: newTodos
      })
    default:
      return state
  }
}

Store

The store is where the state is held. It’s created through the createStore function, which takes the reducer function and the initial state.

import { createStore } from 'redux'
const store = createStore(reducer, ['Use Redux'])

It is used within the Provider react component that will expose it to every child component.

import ReactDOM from 'react-dom';

ReactDOM.render(
  <Provider store={store}>
      <App />
  </Provider>
	, document.getElementById('root'));

Redux Caveats

During my time using Redux I found few things that were not great,

1. Inmutability on reducers

I get it, due to how React works, it’s better if reducers are inmutable. But it is annoying to be remembering all the inmutable functions. Like, did you know slice is an inmutable function but splice is not? Also when modifying the state, results are quite unpredictable. Actually If you notice the example in the Reducer section, dealing with inmutability ends up being most of the code within the reducer.

2. Dependency between actions and reducers

So, most of the time, we will have an action for every case in the reducer. I find this approach rather redundant.

3. Side effects

I think this is a big issue as it’s not only an inconvenience but an actual limiting factor. Every time that we need to coordinate a few asynchronous operations with state changes, side effects are actually required

For example, when we login our users, we might want to:

  • Show a loading indicator while API call is being done
  • Call login API
  • Stop loading indicator
  • If there’s an error
    • display the error
  • If it was successful
    • Request more profile information
    • Redirect to home screen

Redux-toolkit

Redux toolkit greatly simplifies Redux code, and thus solves 1 and 2. I particularly like slices that merge the action definitions with the reducers definitions.

const todosSlice = createSlice({
  name: 'todos',
  initialState: {todos = []},
  reducers: {
    addTodo: (state, action) => {
        state.todos.append(action.payload.text)
    },
    resetTodos: (state, action) => {
        state.todos = []
    }
  }
})

This code is equivalent to all the reducer and actions code we have seen before. It not only reduces line count, but it makes our code more robust, as every action reducer is contained in its own function and state is completely mutable.

Redux-Saga

For this one I’m really excited about, redux saga is a Middleware framework that enables handling side effects in a reliable and easy way. It is daunting at first, my first approach was to use RxJs but I found it’s interface really cumbersome compared to redux-saga .

Redux-saga, uses not a very well known ES6 feature called generator functions. I know, it’s not great to have some syntax that is potentially used only in one part of the codebase.

But believe me it’s worth it. And it’s not that complex.

“Generators are functions that can be exited and later re-entered.”

Basically are functions that use the yield keyword to give control to the redux-saga middleware.

export function* performLogin({payload}) {
  const {username, password} = payload
  try {
    yield put(setIsLoading(true))
    yield call(api.login, {username, password})

    const profile = yield call(api.getProfile)
    cont todos = yield call(api.getTodos)

    yield put(setProfile(profile))
    yield put(setIsLoggedIn(true))
    yield put(setTodos(todos))

    redirect('/')
  } catch (error) {
    yield put(setError(error)
  } finally {
    yield put(setIsLoading(false))
  }
}

export function* watchUserLogin() {
  yield takeEvery(userLogin.type, performLogin)
}

This is the code for the example in the caveats section. It implements a basic login flow:

  • Show a loading indicator while API calls are being done
  • Calls multiple APIs
  • If there’s an error
    • display the error
  • If it was successful
    • Redirects to home screen

Here I’m just using two redux-saga effects call and put. But there’s plenty.

Redux-saga effects basically tell the middleware to do something. put simply puts a new action in the pipeline and call calls an asynchronous function and waits until it’s execution is completed.

Written by Agusti Bau
I'd love to know your thoughts about this post.
Here's my linkedin

© 2024, Agusti Bau