Creating Scalable Stores in React using Hooks and Context API.

The FLUX pattern is a design architecture used to create scalable and well-organized web applications. This pattern was inspired by the Redux architecture, which is based on three key concepts: the store, actions, and reducers.

In ReactJS, it is possible to use the FLUX pattern using only Hooks and Context API. Below are the steps to implement this pattern using these tools:

  • Creating the initial state: The initial state is the state that will be used for the application. This state should be an immutable variable that is in the application's context.
export interface ITodo {
  text: string;
  completed: boolean;
}

export interface ITodoState {
  items: ITodo[];
  isLoading: boolean;
}

const initialState: ITodoState = {
  items: [],
  isLoading: false,
};

export default initialState;
  • Creating actions: Actions are objects that describe what happened in the application. These actions are dispatched by the application's components and are sent to the reducer.
import { ITodo } from "./initialState";

export type TodoTypes = "ADD_TASK" | "REMOVE_TASK" | "TOGGLE_COMPLETE_TASK";

type Action<T> = {
  type: TodoTypes;
  payload: T;
};

interface IAddTask extends Action<ITodo> {
  type: "ADD_TASK";
}

interface IToogleCompleteTask extends Action<number> {
  type: "TOGGLE_COMPLETE_TASK";
}

interface IRemoveTask extends Action<number> {
  type: "REMOVE_TASK";
}

export type TAction = IAddTask | IRemoveTask | IToogleCompleteTask;
  • Creating the reducer: The reducer is a function that receives the current state and the dispatched action. Based on the action, the reducer updates the state and returns the new state.
import { TAction } from "./actions";
import { ITodoState } from "./initialState";

const reducer = (state: ITodoState, action: TAction): ITodoState => {
  const { type } = action;
  switch (type) {
    case "ADD_TASK":
      return { ...state, items: [...state.items, action.payload] };
    case "REMOVE_TASK":
      return {
        ...state,
        items: state.items.filter((_, index) => index !== action.payload),
      };
    case "TOGGLE_COMPLETE_TASK":
      return {
        ...state,
        items: state.items.map((item, index) => {
          if (index === action.payload) {
            return { ...item, completed: !item.completed };
          }
          return item;
        }),
      };
    default:
      return state;
  }
};
export default reducer;
  • Creating the Context:
import { Dispatch, createContext } from 'react'
import { TAction } from './actions'
import initialState, { ITodoState } from './initialState'

interface IContextProps {
  state: ITodoState
  dispatch: Dispatch<TAction>
}
const TodoContext = createContext<IContextProps>({
  state: initialState,
  dispatch: () => {}
})
export default TodoContext
  • Creating the provider: The provider is the component that wraps the application and provides the context to the application. This component uses the useReducer hook of React to update the state based on the dispatched actions.
import React, { FC, useReducer } from "react";

import Context from "./context";
import reducer from "./reducer";
import initialState from "./initialState";

const TodoProvider: FC<{ children: React.ReactNode }> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <Context.Provider value={{ state, dispatch }}>{children}</Context.Provider>
  );
};
export default TodoProvider;
  • Wrap you parent component with TodoProvider:
import TodoContainer from "./components/todo/TodoContainer";
import { TodoProvider } from "./store/todo";
import "./App.css";

function App() {
  return (
    <div className="w-screen h-screen bg-gray-50">
      <TodoProvider>
        <TodoContainer />
      </TodoProvider>
    </div>
  );
}

export default App;
  • Using the store: To use the store, the useContext hook is used to access the state and actions.
import { TodoContext } from "../../store/todo";
import Todo from "./Todo";
import TodoForm from "./TodoForm";

const TodoContainer = () => {
const todoContext = useContext(TodoContext)

  const onAddTodo = (item: string) => {
    todoContext.dispatch({
      type: "ADD_TASK",
      payload: { text: item, completed: false },
    });
  };

  const onDeleteTodo = (index: number) => {
    todoContext.dispatch({ type: "REMOVE_TASK", payload: index });
  };

  const onToogleCompleteTodo = (index: number) => {
    todoContext.dispatch({ type: "TOGGLE_COMPLETE_TASK", payload: index });
  };

  return (
    <div className="w-full h-full flex flex-col justify-center items-center">
      <div className="w-full h-full lg:w-1/4 lg:h-auto min-h-[400px] bg-gray-900 p-8 overflow-x-hidden overflow-y-auto rounded-md shadow-2xl shadow-gray-600">
        <h1 className="text-center text-4xl font-bold mb-4 text-white">
          ✅ Todo List
        </h1>
        <TodoForm handleFormSubmit={onAddTodo} />
        {todoContext.state.items.map((todo, key) => (
          <Todo
            key={key}
            text={todo.text}
            completed={todo.completed}
            onDelete={() => onDeleteTodo(key)}
            onComplete={() => onToogleCompleteTodo(key)}
          />
        ))}
      </div>
    </div>
  );
};

export default TodoContainer;

In summary, by using ReactJS Hooks and Context API, it is possible to implement the FLUX pattern in a simple and scalable way. By following the steps described above, it is possible to create an application with a clear and easy-to-understand structure.

To see a full example review this template react-hooks-store-starter Clone the project:

git clone https://github.com/gsi-chao/react-hooks-store-starter.git

Or create a new project with this repository using degit:

npx degit gsi-chao/react-hooks-store-starter the-react-store-way