Muhi Logo Text
AboutBlogWork With Me

How to Create Dynamic States with React Redux

Learn how to create dynamic states for editable lists and tables with React Redux

Last updated on October 21, 2021

react
architecture
React Redux

Redux is one of the most popular state management libraries in the React ecosystem. If you’re not familiar with the state management concept, it’s a layer added to your project that maintains and shares data across all components.

In Redux, we typically create predefine states in the reducer and dispatch required updates to all subscribed components. For example:

const initialState = {
  title: "Do groceries",
  completed: false,
};

export const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case "UPDATE_TITLE": {
      return {
        ...state,
        title: action.value,
      };
    }
    default:
      return state;
  }
};

The above code is a simple reducer that updates a to-do item. Usually, we have a dynamic to-do list so instead, we can store a list and return a new immutable version when the user updates a value:

const initialState = {
  todoList: [
    { id: 1, title: "Do groceries", completed: false },
    { id: 2, title: "Do shopping", completed: false },
  ],
};

export const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case "UPDATE_TODO_LIST": {
      return {
        ...state,
        todoList: [...action.value],
      };
    }
    default:
      return state;
  }
};

And the component will have a change handler that updates the item based on the id. Afterwards, it will dispatch the changes:

export const TodoList = () => {
  const todoList = useSelector(state => state.todoList)
  const dispatch = useDispatch();

  function changeHandler(e, id) {
    const todoItem = todoList.find(item => item.id === id);
    todoItem.title = e.target.value;
    dispatch({
      type: "UPDATE_TODO_LIST",
      value: todoList,
    });
  }
  return (
    <article>
      {todoList.map((item) => (
        <section key={item.id}>
          <input type="text" value={item.title} onChange={(e) => changeHandler(e, item.id)} />
        </section>
      ))}
    </article>
  );
};

Although this approach works just fine and React’s virtual DOM is efficient when re-rendering lists, we might get performance issues as the application grows bigger and the data structure becomes more complex. Especially when the component is a multi-column editable grid that contains nested children.

In this tutorial, we will explain how to dynamically create and update states in Redux that will enable us to surgically edit data in a grid without re-rendering the whole list.

The tutorial assumes that you have a basic understanding of Redux. If not, please go through the overview on the official site.

Create a basic todo component

We are going to create a component with a real API call and display the list in a table element. Let’s go through the required steps:

  1. Create an async function to trigger an API call. We’re going to use {JSON} Placeholder service that returns a fake to-do list.
async function getTodoList() {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos");
}
  1. Add the API function in the useEffect hook to be triggered when the component is ready and then update the todoList state
useEffect(() => {
  async function getTodoList() {
    const response = await fetch("https://jsonplaceholder.typicode.com/todos");
    setTodoList(await response.json());
  }
  getTodoList();
}, []);
  1. Create a table element and loop through the data to return dynamic tr and td elements
<table>
  <thead>
    <tr>
      <th>Task</th>
      <th>Completed</th>
    </tr>
  </thead>
  <tbody>
    {todoList.map((item) => (
      <tr key={item.id}>
        <td>{item.title}</td>
        <td>{item.completed}</td>
      </tr>
    ))}
  </tbody>
</table>
Full component
import { useState, useEffect } from "react";
import "./todo.css";

export const TodoList = () => {
  const [todoList, setTodoList] = useState([]);

  useEffect(() => {
    async function getTodoList() {
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/todos"
      );
      setTodoList(await response.json());
    }
    getTodoList();
  }, []);

  return (
    <table>
      <thead>
        <tr>
          <th>Task</th>
          <th>Completed</th>
        </tr>
      </thead>
      <tbody>
        {todoList.map((item) => (
          <tr key={item.id}>
            <td>{item.title}</td>
            <td>{item.completed}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

So far, we have a simple two-column and read-only to-do list. Keeping reading, it will get more exciting I promise 😊

Add dynamic states

The key to updating a specific item without re-rendering the whole list is to create dynamic states based on ids (similar to a dictionary). The initial state in the reducer will be an empty object and as soon as the data is ready in the component we’ll re-generate the data as a key/value based object, for example:

{
  1: {
    title: 'Do groceries',
    completed: false
  },
  2: {
    title: 'Do shopping',
    completed: true
  },
  3: {
    title: 'Go to the gym',
    completed: false
  }
}

Let’s create a reducer with a new action type that updates the data list:

const initialState = {};

export const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case "UPDATE_TODO_LIST": {
      return action.value.reduce(
        (accumulator, current) => ({ ...accumulator, [current.id]: current }),
        {}
      );
    }
    default:
      return state;
  }
};

As you can see above, we used the array’s reduce function to re-generate the data to an id-based object.

Next, in the TodoList component, we’ll dispatch the data right after the API call.

useEffect(() => {
  async function getTodoList() {
    const response = await fetch("https://jsonplaceholder.typicode.com/todos");
    const data = await response.json();
    dispatch({
      type: "UPDATE_TODO_LIST",
      value: data,
    });
    setTodoList(data);
  }
  getTodoList();
}, []);

As soon as the component is ready, the reducer will convert the to-do list to a new object and update the state. Keep in mind that the process of calling the API and converting the data will only happen once.

Update input fields

Updating a specific input field in the table list is now possible without re-rendering the Todo component. We first need to add a new action type in the reducer that will update a specific id and field as follows:

const initialState = {};

export const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case "UPDATE_TODO_LIST": {
      return action.value.reduce(
        (accumulator, current) => ({ ...accumulator, [current.id]: current }),
        {}
      );
    }
    case "UPDATE_TODO_FIELD": {
      return {
        ...state,
        [action.id]: {
          ...state[action.id],
          [action.field]: action.value,
        },
      };
    }
    default:
      return state;
  }
};

Using JavaScript’s dynamic key assignment and spread operator, we can modify the field of a specific to-do item. What is left now is to create custom input field components for both the “title” and “completed” properties. Let’s go through the required steps:

  1. Add two separate components, a checkbox for the “completed” field and an input text for the “title” field:
const TextBox = () => {
  return <input type="text" />
}

const CheckBox = () => {
  return <input type="checkbox" />
}
  1. Add useSelector that subscribes to an id and field:
const TextBox = ({id}) => {
  const value = useSelector(state => state[id]?.title)
  return <input type="text" value={value}  />
}

const CheckBox = ({id}) => {
  const value = useSelector(state => state[id]?.completed);
  return <input type="checkbox" value={value} />
}

We’re accessing ids directly from the state (state[id]) as we only have one main reducer in this example but in a real-life scenario, we’d probably have multiple combined reducers that can be accessed by name (state.myReducerName[id])

  1. Dispatch input changes to the new action that we created in the previous step and pass it the id, field, and value:
const TextBox = ({id}) => {
  const value = useSelector(state => state[id]?.title)
  const dispatch = useDispatch();
  function changeHandler(e) {
    dispatch({
      type: 'UPDATE_TODO_FIELD',
      id, value: e.target.value, field: 'title'
    })
  }
  return <input type="text" value={value} onChange={changeHandler} />
}

const CheckBox = ({id}) => {
  const value = useSelector(state => state[id]?.completed);
  const dispatch = useDispatch();
  function changeHandler(e) {
    dispatch({
      type: 'UPDATE_TODO_FIELD',
      id, value: e.target.checked, field: 'completed'
    })
  }
  return <input type="checkbox" value={value} onChange={changeHandler} />
}
Full component
import { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import "./todo.css";

export const TodoList = () => {
  const [todoList, setTodoList] = useState([]);
  const dispatch = useDispatch();

  useEffect(() => {
    async function getTodoList() {
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/todos"
      );
      const data = await response.json();
      dispatch({
        type: 'UPDATE_TODO_LIST',
        value: data
      });
      setTodoList(data);
    }
    getTodoList();
  }, []);

  return (
    <table>
      <thead>
        <tr>
          <th>Task</th>
          <th>Completed</th>
        </tr>
      </thead>
      <tbody>
        {todoList.map((item) => (
          <tr key={item.id}>
            <td><TextBox id={item.id}></TextBox></td>
            <td><CheckBox id={item.id}></CheckBox></td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

const TextBox = ({id}) => {
  const value = useSelector(state => state[id]?.title)
  const dispatch = useDispatch();
  function changeHandler(e) {
    dispatch({
      type: 'UPDATE_TODO_FIELD',
      id, value: e.target.value, field: 'title'
    })
  }
  return <input type="text" value={value} onChange={changeHandler} />
}

const CheckBox = ({id}) => {
  const value = useSelector(state => state[id]?.completed);
  const dispatch = useDispatch();
  function changeHandler(e) {
    dispatch({
      type: 'UPDATE_TODO_FIELD',
      id, value: e.target.checked, field: 'completed'
    })
  }
  return <input type="checkbox" value={value} onChange={changeHandler} />
}

Demo

Summary

When the TodoList component first renders and dispatches the data, we’re creating a dictionary based on ids. Initially, all input fields will be updated as they’re subscribed to that object. But, whenever the user does any modifications, only the component subscribed to the specific id/field will be rendered leaving us with optimum performance 🚀

Bye for now 👋

If you enjoyed this post, I regularly share similar content on Twitter. Follow me @muhimasri to stay up to date, or feel free to message me on Twitter if you have any questions or comments. I'm always open to discussing the topics I write about!

Recommended Reading

Learn how to create encapsulated and reusable Fieldset component with Material UI (MUI) and React.

react
mui

Discussion

Upskill Your Frontend Development Techniques 🌟

Subscribe to stay up-to-date and receive quality front-end development tutorials straight to your inbox!

No spam, sales, or ads. Unsubscribe anytime you wish.

© 2024, Muhi Masri