I’ve been involved in - projects for several years. After I first met with flux, I was impressed by its expressive power that describes complicated use cases in contrast to other dataflow concepts, which caused many troubles when the complexity of a project increased. react redux Data changes can be described as actions with a minimal payload. These actions make a deterministic, time-independent history of the application’s life. The application’s state at a given point is reducible by picking an action in the chain. The action controlled dataflow concept is simple and clear. The concept of Redux has many theoretical principles and advantages, but I do not intend to talk about them. There is only one major disadvantage of immutability: the cost of it. But We can always keep track of the difference between two consecutive states, and that is something why I cannot list immutability as a disadvantage of Redux. the price we must pay for immutable data handling is multiple refunded by avoiding re-renders and reflows in React applications. Motivation Redux has one more disadvantage: it is painfully verbose. Let’s suppose we want to create an async action, which fetches users and saves them in a Redux store instance. We need 3 action definitions: const START_FETCHING_USERS = "START_FETCHING_USERS"; const RESOLVE_FETCHING_USERS = "RESOLVE_FETCHING_USERS"; const REJECT_FETCHING_USERS = "REJECT_FETCHING_USERS"; The first action type starts the process, provides the new set of users, and is emitted if there is an error during fetching. START_FETCHING_USERS RESOLVE_FETCHING_USERS REJECT_FETCHING_USERS Let’s see the action creators: const startFetchingUsers = () => ({ type: START_FETCHING_USERS }); const resolveFetchingUsers = users => ({ type: RESOLVE_FETCHING_USERS, users }); const rejectFetchingUsers = error => ({ type: RESOLVE_FETCHING_USERS, error }); and the reducer: const initState = { isFetching: false, users: [], error: null} const reducer = (state = initState, action) => { switch (action.type) { case START_FETCHING_USERS: return { ...state, isFetching: true }; case RESOLVE_FETCHING_USERS: return { ...state, isFetching: false, users: action.users }; case REJECT_FETCHING_USERS: return { ...state, isFetching: false, error: action.error }; default: return state; }} All that remains is to implement the async action creator: thunk const fetchUsers = () => async (dispatch, getState, { api }) => { dispatch(startFetchingUsers()); try { const users = await api.get('/users'); dispatch(resolveFetchingUsers(users)); } catch (error) { dispatch(rejectFetchingUsers(error.message)); }} Okay, we finished the Redux parts & we’re almost done. Now we just need to connect the action creators and the state to the React component, and we are good to go! For this simple feature, we needed to type a lot of lines for action types, action creators, action handlers in the reducer, and we have not written any view components yet. This is especially inconvenient when we are involved in developing a large application with thousands of action types, action creators, and sub-reducers. It causes further difficulties too, because these resources are separated in many files, in different places. So if we want to trace the effect of an action, we have to follow the flow of data across many files, which makes it easy to get lost. By looking around in , we are most likely to find a bunch of libraries/helpers/middlewares, which help us to avoid typing, but using them introduces some other type of typing overhead as we need to import them in every file. npm Maybe we should think of a simpler way and consider which features we really need from Redux. Mutability is the highway to hell. So this is not a solution. Especially not in React applications. Do we have to keep the data immutable? In most cases, the actions are used only in single place. We do not need to keep them reproducible. What if you have a way to dispatch anonymous actions? Do we have to know the name of an action? This would be great. There are use cases where you absolutely need to be serializable, but in most applications, you do not. So let’s continue with the assumption that this is not a requirement for now. Do we have to be able to serialize the actions? We should adhere to the first restriction, while we can safely forget the others. We should transform the Redux concepts to make it possible that we can create actions briefly. We want to describe an action as a single function, either in place. Repatch drops action types and action creators from the definition set, and answers the question: “What if reducers were the payload of the actions?”. The creed of this library is: Repatch DISPATCH REDUCERS store.dispatch(state => ({ ...state, counter: state.counter + 1 })); In this terminology, an action is a function that returns a reducer: const increment = amount => state => ({ ...state, counter: state.counter + amount}); store.dispatch(increment(42)); Repatch also has a class that we can instantiate with the initial state: Store import Store from 'repatch'; const store = new Store(initialState); Repatch’s interface is very similar as ’s, therefore we can use it with the library. The and methods have the same signature as in the Redux's . redux react-redux dispatch subscribe Store Middlewares and Async Actions Repatch also has an interface for chaining middlewares. This is convenient for using your favorite async-action middleware. The package provides a middleware - similar to - which is useful for creating async actions. If your reducer returns a function, it will be automatically considered an async action by the middleware. The and functions will be passed as arguments to it by the store instance. You can set up the middleware to provide one extra argument to. You can use that, for example to inject your client API library. thunk redux-thunk dispatch getState Let’s see the example related to our use-case below: const fetchUsers = () => _ => async (dispatch, getState, { api }) => { dispatch(state => ({ ...state, isFetching: true })); try { const users = await api.get('/users'); dispatch(state => ({ ...state, users })); } catch (error) { dispatch(state => ({ ...state, error: error.message })); } finally { dispatch(state => ({ ...state, isFetching: false })) }} Using this middleware shows the real power of repatch as we can describe async actions in only a few lines of code. As you can see, we did not need to define verbose action types, action creators and action handlers in the reducer, as we could simply dispatch an arrow function defined in place, thus creating an . How cool is that? This makes it possible that actions either can also be created from a component. thunk anonymous action All that remains is the instantiation with the initial state: Store const store = new Store({ isFetching: false, users: [], error: null}); and somewhere dispatching the action: store.dispatch(fetchUsers()) Let’s see an other example: const updateUser = delta => state => async (dispatch, getState, { api }) => { try { const editedUserId = getState().editedUser; dispatch(toggleSpinner(true)); await api.put(`/users/${editedUserId}`, { body: delta }); await dispatch(fetchUsers()); dispatch(toggleSpinner(false)); } catch (error) { dispatch(state => ({ ...state, isFetching: false, error: error.message })); }}; You can see from the function signature that in this example the extra argument is our client API object, as I mentioned previously. Also, note that the reducer’s argument is not always satisfactory for reading the state because it is a momentary representation from the time when the action was fired. Therefore we need to use the function instead of . state getState state In this example, is a regular synchronous action that we can . The method is a simple async method to call the API, there is no obstacle in the way of ing for it. The line is a bit more interesting. Using we got used to embedding async actions within each other and waiting for them. toggleSpinner dispatch api.put await await dispatch(fetchUsers()) redux-thunk Sub-reducers in Redux Redux’s reducers are composable to form a hierarchical structure. This way we do not need to define one giant reducer, instead, we can separate them to smaller nested reducers. Combining reducers is not magic, we just create a reducer that reduces the parts one by one to an object using their sub-state. const rootReducer = (state, action) => ({ foo: fooReducer(state.foo, action), bar: barReducer(state.bar, action)}); is equivalent to const rootReducer = redux.combineReducers({ foo: fooReducer, bar: barReducer}); Sub-reducers in Repatch Repatch also offers a way to combine sub-reducers. We just define a function that takes a nested reducer as argument, and returns a reducer that reduces the whole state: const reduceFoo = fooReducer => state => ({ ...state, foo: fooReducer(state.foo)}); Now reducing the property is easy. Let's suppose we would like to set an property in the object: foo x foo const setX = x => reduceFoo(state => ({ ...state, x })); It will be really useful if the sub-reducer describes a deeply nested property: const reduceFoo = reducer => state => ({ ...state, bar: { ...state.bar, foo: reducer(state.bar.foo) }}); Testing How about testing? Writing unit tests for a reducer is simple: import * as assert from 'assert'; import { changeName } from './actions'; // ... it('changeName', () => { const state = { name: 'john' }; const nextState = changeName('jack')(state); assert.strictEqual(nextState.name, 'jack');}); Async actions are a bit more complicated because they take effect by depending on external resources such as the store instance and other APIs. But external resources always need to be mocked in all environments. import Store, { thunk } from 'repatch'; import * as assert from 'assert'; const mockUsers = [{ username: 'john' }]; const mockApi = { getUsers: () => Promise.resolve(mockUsers)} // ... it('fetchUsers', async () => { const state = { users: [] }; const store = new Store(state) .addMiddleware(thunk.withExtraArgument({ api: mockApi })); await store.dispatch(fetchUsers()); const nextState = store.getState(); assert.deepEqual(nextState.users, mockUsers);}); The TODO app Every javascript library has a todo example, so repatch has too. If you are looking for the TypeScript example, you can find it . one here This Guest Post written by Péter Hauszknecht was Originally published at community.risingstack.com on August 16, 2017.
Share Your Thoughts