If we think of the redux store as a database then a reducer can be thought of as a table. The most similar data structure to a table is a json object where the keys are ids and the values are json objects. We have created a slice helper that creates some very common actions and selectors that manage that table.
NOTE: We do not use immer
for any slice helpers. Since they are highly
reusable pieces of code, we are comfortable properly handling reducer logic
without the performance overhead of immer
This is probably the most useful slice helper provided by robodux
. It
creates a redux slice that acts like a database table. The key is the id of
the record and the value is the record itself. You are responsible for
setting the object up as robodux
has no way to know what the record id is.
We could have created a function that you provide that tells us what the id
is but we made the design decision to keep this API interface simple.
import { createTable } from 'robodux';
interface User {
name: string;
email: string;
posts: string[];
}
const name = 'users';
const { reducer, actions, getSelectors } = createTable<User>({ name });
const state = {
3: { name: 'three', email: 'three@three.com', posts: ['1'] }
};
store.dispatch(
actions.add({
1: { name: 'one', email: 'one@one.com', posts: [] },
2: { name: 'two', email: 'two@two.com', posts: [] },
})
);
/* {
1: { name: 'one', email: 'one@one.com', posts: [] },
2: { name: 'two', email: 'two@two.com', posts: [] },
3: { name: 'three', email: 'three@three.com', posts: ['1'] },
} */
store.dispatch(
actions.set({
4: { name: 'four', email: 'four@four.com', posts: ['3'] },
5: { name: 'five', email: 'five@five.com', posts: [] },
6: { name: 'six': email: 'six@six.com', posts: [] },
})
)
/* {
4: { name: 'four', email: 'four@four.com', posts: ['3'] },
5: { name: 'five', email: 'five@five.com', posts: [] },
6: { name: 'six': email: 'six@six.com', posts: [] },
} */
store.dispatch(
actions.remove(['5', '6'])
)
/* {
4: { name: 'four', email: 'four@four.com', posts: ['3'] },
} */
// only update a part of the entity
store.dispatch(
actions.patch({
4: { name: 'five' }
})
)
/* {
4: { name: 'five', email: 'four@four.com', posts: ['3'] },
} */
// patch + 1-level merging of objects and arrays within the record
store.dispatch(
actions.merge({
4: { posts: ['5', '6'] }
})
);
/* {
4: { name: 'five', email: 'four@four.com', posts: ['3', '5', '6'] },
} */
store.dispatch(
actions.reset()
)
// {}
const state = store.getState();
const selectors = getSelectors((state) => state.user);
// returns the entire slice of data
const users = selectors.selectTable(state);
// returns all slice data as an array
const userList = selectors.selectTableAsList(state);
// will return the record at the id specified or undefined if it is not found
const userOne = selectors.selectById(state, { id: '1' });
const defaultUser: User = {
name: '',
email: '',
}
const createEntitySelector = mustSelectEntity(defaultUser);
const selectById = createEntitySelector(selectors.selectById);
// must return User even if one isn't found
const userSix = selectors.selectById(state, { id: '6' });
// get users by list of ids
const usersById = selectors.selectByIds(state, { ids: ['1', '6'] });
These are common operations when dealing with a slice that simply needs to be set or reset. You can think of this slice helper as a basic setter. I regularly use this for simple types like strings or if I'm prototyping and I just need something quick.
import { createAssign } from 'robodux';
const name = 'token';
const { reducer, actions } = createAssign<string>({
name,
initialState: '',
});
store.dispatch(actions.set('some-token'));
/* redux state: { token: 'some-token' } */
store.dispatch(actions.set('another-token'));
/* redux state: { token: 'another-token' } */
store.dispatch(actions.reset());
// redux state: { token: '' }
This is an array data structure where it's easy to manage a simple array.
import { createList } from 'robodux';
const { reducer, actions } = createList({ name: 'userIds' });
store.dispatch(actions.add(['1', '2']));
/*
{
userIds: ['1', '2']
}
*/
store.dispatch(actions.add(['3']));
/*
{
userIds: ['1', '2', '3']
}
*/
store.dispatch(actions.remove(['1', '2']));
/*
{
userIds: ['3']
}
*/
store.dispatch(actions.reset());
/*
{
userIds: []
}
*/
This is a table of loaders so we can build an infinite number of loaders for our app keyed by the id.
const { createLoaderTable } from 'robodux';
const { actions, reducer } = createLoaderTable({ name: 'loaders' });
store.dispatch(actions.loading({ id: 'users', message: 'fetching users ...' }));
/*
{
loaders: {
users: {
error: false, message: 'fetching users ...', loading: true, success: false, lastRun: 11111111, lastSuccess: 0
}
}
}
*/
store.dispatch(actions.success({ id: 'users' }));
/*
{
loaders: {
users: {
error: false, message: 'fetching users ...', loading: false, success: true, lastRun: 11111111, lastSuccess: 22222222
}
}
}
*/
store.dispatch(actions.error({ id: 'users', message: 'something happened' }));
/*
{
loaders: {
users: {
error: true, message: 'something happened', loading: false, success: false, lastRun: 11111111, lastSuccess: 22222222
}
}
}
*/
store.dispatch(actions.loading({ id: 'posts' }));
/*
{
loaders: {
users: {
error: true, message: 'something happened', loading: false, success: false, lastRun: 11111111, lastSuccess: 22222222
},
posts: {
error: false, message: '', loading: true, success: false, lastRun: 33333333, lastSuccess: 0
}
}
}
*/
This is a very useful function that will convert a list of slices into an
object of reducers where the key is the name of the slice and the value is the
reducer itself. This allows us to compose slices together and prepare it for
combineReducers
.
import { combineReducers } from 'redux';
import { createTable, createAssign, createReducerMap } from 'robodux';
const users = createTable({ name: 'users' });
const threads = createTable({ name: 'threads' });
const comments = createTable({ name: 'comments' });
const token = createAssign({ name: 'token', initialState: '' });
const reducers = createReducerMap(users, threads, comments, token);
/*
{
users: Reducer,
threads: Reducer,
comments: Reducer,
token: Reducer,
}
*/
const roorReducer = combineReducers(reducers);
If we are following the robodux
modular
pattern
then when we are building our slices they live within their own modules. Each
module exports a variable reducers
which contains all the reducers created
within the module. When we want to build our root reducer, we need a way to
combine all the module reducers into one reducer. createApp
helps combine
all the reducers from all modules into a single reducer.
import { createStore } from 'redux';
import { createApp } from 'robodux';
import * as users from '@app/users';
import * as threads from '@app/threads';
import * as comments from '@app/comments';
import * as token from '@app/token';
const { reducer } = createApp([users, threads, comments, tokens]);
const store = createStore(reducer);
By default createSlice
will prefix any action type with the name of the slice.
However, sometimes it is necessary to allow external action types to effect the
reducer. All slice helpers also accept extraReducers
which will be passed
through to createSlice
.
const user = createSlice<User, UserActions>({
name: 'user',
initialState: { name: '', address: '' },
reducers: {
setUserName: (state, payload) => {
state.name = payload; // mutate the state all you want with immer
},
},
extraReducers: {
setAddress: (state, payload) => {
state.address = payload;
},
},
});
store.dispatch({ type: 'setAddress', payload: '1337 tsukemen rd' });
store.getState();
// { name: '', address: '1337 tsukemen rd' }
All of our slice helpers also accept extraReducers
.