Our custom redux abstraction
This article assumes you know the basic data flow and structure of redux. Read the docs if you don't, or just need to remember.
Why it's abstracted in the first place
The store+reducer+actions structure used by redux is unquestionably a great way to manage global state in a predictable and scalable fashion. However, it's adoption causes the codebase to become cluttered with boilerplate code and allows for plenty of developer error(when specifying action types for example).
Redux toolkit solves both of these issues (and some others), but doesn't include a simple way to handle asynchronous operations inside actions, as evidenced by this part of the redux toolkit docs.
The redux team have also create Redux toolkit query, which aims to solve the problem described in the paragraph above. It's problem is that it's too opinionated, and does not allow ease of use when dispatching actions outside of components, which is a surprisingly convenient feature to have when developing apps with lots of third-party libraries and concurrent operations.
With these issues in mind, Codeleap's flavor of redux was created.
The recipe
These are the problems we need to solve:
Quite the easy challenge no?
Well, maybe there's a better way to solve it, but this is the best solution we came up with it. It's heavily inspired by Redux toolkit, and actually uses some of it's types.
The code below won't walk you through the implementation details, but if you wish to dive deeper, look here.
In a real world scenario, the example below would be best solved by using react-query, but it's made this way to conform to the most common examples of CRUD architecture.
// redux/todos.tsimport {ApiClient} from '@/services'import { createSlice } from '@codeleap/common'export type Todo = {id: numbercreated_datetime: stringtitle: stringnote: string}type TodosState = {todos: Todo[]loading: booleanerror: {message: string} | null}const initialState: TodosState = {todos: [],loading: false,error: null,}type PaginationParams = {limit?: numberoffset?: number}// The createSlice function is simply a wrapper around redux's dispatch, which creates an internal reducer to match action types the functions specified withing "reducers" and "asyncReducers"export const todosSlice = createSlice({name: 'Todos', // this is needed for creation of action namesinitialState,reducers: {setTodo(state, value: Partial<Todo>){ // "value" here is the action's payloadconst todoIdx = state.todos.findIndex(todo => todo.id === value.id)const newTodos = [...state.todos]newTodos[todoIdx] = valuereturn {todos: newTodos}}},asyncReducers: {list: async (state, setState, params: PaginationParams) => {// This simply dispatches an action under the hood.// But by using a function in this manner, we are able to write async code without thunks or sagassetState({loading: true})const todos = ApiClient.todos.list(params)setState({todos: [...state.todos, ...todos],loading: false})},create: async (state, setState, params: Omit<Todo,'id'>) => {setState({loading: true})const newTodo = ApiClient.todos.create(params)setState({todos: [...state.todos, newTodo],loading: false})}}})// redux/index.tsimport { createRedux } from '@codeleap/common'import { todosSlice } from './todos'import { TypedUseSelectorHook, useSelector } from 'react-redux'export const {store,actions: { Todos },} = createRedux({Todos: todosSlice})export type RootState = ReturnType<typeof store.getState>export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
And use it in the component like so
// TodoList.tsximport { useAppSelector, Todos, Todo } from '@/redux' // This is just an alias to the redux folderimport { onMount } from '@codeleap/common'export const TodoList = () => {const {todos} = useAppSelector(store => store.Todos)onMount(() => {Todos.list()})function createTodo(values: Todo){Todos.create(values).then((newTodo) => {console.log('New todo created', newTodo)})}return <><TodoForm onSubmit={createTodo}/>{todos.map(todo => <Todo item={todo} key={todo.id}/>)}</>}
As you can see, there's still a lot of code, but it's significantly less verbose and complicated than redux, doesn't leave a lot of room for error, and integrates nicely with asynchronous API calls while providing maximum control to the developer