umma.dev

State Management in React

Rememeber Redux and MobX? Well guess what, there are even MORE choices to pick from when it comes to implementing global state within React applications!

Redux

Global state management.

Pros
  • Increases the predictability of a state
  • Highly maintainable
  • It prevents re-renders
  • Optimises performance
  • Makes debugging easier
  • Useful in server-side rendering
  • Provides ease of testing
Cons
  • Lack of encapsulation
  • Restricted design
  • Excessive memory use
  • Increased complexity
  • Time consuming
Best For
  • Large and complex applications, with many moving parts
Implementation

Store

import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers/rootReducer";

export default function configureStore() {
  return createStore(rootReducer, applyMiddleware(thunk));
}

Reducers

//src/reducers/testReducer.js
export default(state = {}, action => {
    switch(action.type) {
        case 'THE_TEST':
            return {
                ...state,
                test: action.payload
            }
        default:
            return state
    }
})

//src/reducers/rootReducer.js
import { combineReducers } from 'redux'
import testReducer from './testReducer'

export default combineReducers({
testReducer
});

Actions

import { THE_TEST } from "./constants";

export const theTest = (payload) => {
  return {
    type: THE_TEST,
    payload,
  };
};

export const getTest = () => {
  return function (dispatch) {
    dispatch(theTest());
  };
};

Dispatching Actions in Components

import { getTest } from "../actions/testAction";
import { connect } from "react-redux";

const mapStateToProps = (state) => ({
  ...state,
});

const mapDispatchToProps = (dispatch) => ({
  getTest: () => getTest(testAction()),
});

class testComponent extends Component {
  simpleTest = (event) => {
    this.props.getTest();
  };
  render() {
    return <button onClick={simpleTest}> Test </button>;
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(testComponent);

Redux Toolkit

A wrapper around Redux.

Pros
  • If you know Redux, RTK is pretty much the same and it what’s officially recommended to write any Redux code
  • Saves time
  • Reduces boiler plate coce
  • Simplified store set up
  • Predictability - use your store as a single source of truth
Cons
  • Maintainability - Redux has strict guidelines about how code is organised
  • Prop drilling
Best For
  • Large/complex applications
Implementation
npm install @reduxjs/toolkit react-redux

You can create a boiler plate react app with redux

npx create-react-app app-name --template redux
import { Provider } from "react-redux";
import { store } from "./app/store";
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  value: 0,
};

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
  },
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./slices/counterSlice";
export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement } from "./app/slices/counterSlice";

function App() {
  const dispatch = useDispatch();
  const count = useSelector((state) => state.counter.value);

  return (
    <div className="App">
      <div>{count}</div>
      <div>
        <button onClick={() => dispatch(decrement())}>- 1</button>
        <button onClick={() => dispatch(increment())}>+ 1</button>
      </div>
    </div>
  );
}

Valtio

Proxy state.

Pros
  • Two state updating models; immutable updates and mutable updates
Cons
  • Less predictability due to proxy-based render optimisaton
  • Hard to debug as proxies take care of render optimisation behind the scenes
Best For
  • Proxies
Implementation
import "./styles.css";
import React, { useEffect, useRef, useState } from "react";
import { proxy, useSnapshot } from "valtio";

type Todo = {
  id: number,
  title: string,
  completed: boolean,
};

const state = proxy < { todo: Todo | null } > { todo: null };

const setTodo = (todo: Todo) => {
  state.todo = todo;
};

const updateTodoTitle = (title: string) => {
  if (state.todo !== null) {
    state.todo.title = title;
  }
};

const updateCompleted = () => {
  if (state.todo !== null) {
    state.todo.completed = !state.todo.completed;
  }
};

const App = React.memo(() => {
  const snap = useSnapshot(state);
  const [title, setTitle] = useState("");
  const rerenderCountRef = useRef(0);
  const handleDataFetching = async () => {
    const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
    const todo = await res.json();
    setTodo(todo);
  };
  useEffect(() => {
    rerenderCountRef.current += 1;
  });
  return (
    <div className="App">
      <div style={{ marginBottom: 10 }}>
        <b>Render count:</b> {rerenderCountRef.current}
      </div>
      <button style={{ marginBottom: 10 }} onClick={handleDataFetching}>
        Fetch data
      </button>
      {snap.todo !== null ? (
        <div>
          <p>
            <b>Todo title from valtio:</b> {snap.todo.title}
          </p>
          <form
            onSubmit={(e) => {
              e.preventDefault();
              updateTodoTitle(title);
            }}
            style={{ marginBottom: 10 }}
          >
            <label>
              New title to use in valtio{" "}
              <input
                value={title}
                onChange={(e) => {
                  setTitle(e.target.value);
                }}
              />
            </label>
            <button>Save to global store</button>
          </form>
          <button type="button" onClick={updateCompleted}>
            Toggle todo's completed value
          </button>
        </div>
      ) : (
        <div>Store is empty</div>
      )}
    </div>
  );
});

export default App;

RTK Query

Caching/fetching data.

Pros
  • Caching and fetching data
Cons
  • Still in early stages
Implementation
import { createApi, fetchBaseQuery } from "@rtk-incubator/rtk-query";

// Create your service using a base URL and expected endpoints
export const dndApi = createApi({
  reducerPath: "dndApi",
  baseQuery: fetchBaseQuery({ baseUrl: "https://www.dnd5eapi.co/api/" }),
  endpoints: (builder) => ({
    getMonstersByName: builder.query({
      query: (name: string) => `monsters/${name}`,
    }),
  }),
});

export const { useGetMonstersByNameQuery } = dndApi;
export const store = configureStore({
  reducer: { [dndApi.reducerPath]: dndApi.reducer },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(dndApi.middleware),
});
import * as React from "react";
import { useGetMonstersByNameQuery } from "./services/dnd";

export default function App() {
  const { data, error, isLoading } = useGetMonstersByNameQuery("aboleth");
  return (
    <div className="App">
      {error ? (
        <>Oh no, there was an error</>
      ) : isLoading ? (
        <>Loading...</>
      ) : data ? (
        <>
          <h3>{data.name}</h3>
          <h4> {data.speed.walk} </h4>
        </>
      ) : null}
    </div>
  );
}