I like Redux. It is simple and powerful.
But when I started using it in real word, one architectural question drives me mad.
How to locate my business logic in one place?
Because I have 2 possible places where to locate it:
Action Creators (AC)
Store Reducers (SR)
[AC] -> [Action] -> [SR]
Below is 3 examples.
Ex. 1 and 2 - locate business decisions in AC and SR in sync scenario.
Ex. 3 - business decision made in AC in async scenario.
In my project I've noticed how business decisions are getting scattered between AC and SR very quickly. So each time I want to debug something I should ask myself - ok, so where that decision I want to check is located, AC or SR?
From architectural point of view, I'd rather want to split BL by domains, not by AC/SR.
My point: while I understand advantages of pure reducers that make hot-reloading, time-travel, undo/redo features possible, I'm not sure I'm ready to trade logic maintainability for that.
Still, I have only one week with Redux.
What have I missed?
Example 1 (sync, decisions are in reducer):
// action-creators.js
export function increment() {
return {
type: 'INCREMENT'
}
}
export function decrement() {
return {
type: 'DECREMENT'
}
}
// counter-reducer.js
export default function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1 // (!) decision of how to '(in|de)crement' is here
case 'DECREMENT':
return state - 1
default:
return state
}
}
Example 2 (sync, decisions are in action creators):
// action-creators.js
export function increment() {
return (dispatch, getState) => {
return {
type: 'CHANGE_COUNTER',
newValue: getState() + 1 // (!) decision of how to '(in|de)crement' is here
}
};
}
export function decrement() {
return (dispatch, getState) => {
return {
type: 'CHANGE_COUNTER',
newValue: getState() - 1
}
};
}
// counter-reducer.js
export default function counter(state = 0, action) {
switch (action.type) {
case 'CHANGE_COUNTER':
return action.newValue
default:
return state
}
}
Example 3 (async, decisions are in action creators):
// action-creators.js
export function login() {
return async (dispatch, getState) => {
let isLoggedIn = await api.getLoginState();
if (!isLoggedIn) { // (!) decision of whether to make second api call or not
let {user, pass} = getState();
await api.login(user, pass)
}
dispatch({
type: 'MOVE_TO_DASHBOARD'
})
};
}
// some-reducer.js
export default function someReducer(state = 0, action) {
switch (action.type) {
case 'MOVE_TO_DASHBOARD':
return {
...state,
screen: 'dashboard'
}
default:
return state
}
}
You've pretty well covered the possibilities. Unfortunately, there is no one-size-fits-all answer. It's your app, you'll have to decide.
The Redux FAQ answer on structuring business logic has a good quote on the topic, and links to a few related discussions.
Related
Documentation said i should avoid state mutation by using new Date, etc inside reducers. Help me please with advice how should it be done.
Action:
const RECEIVE_PRICES = 'RECEIVE_PRICES';
function receivePrices(prices) {
return {
type: RECEIVE_PRICES,
receivedAt: Date.now(),
prices,
};
}
REDUCER:
...
case RECEIVE_PRICES: {
let { prices } = action;
prices = prices.map((p) => {
const baseQuote = p.symbol.split('/');
return { ...p, baseCurrency: baseQuote[0], quoteCurrency: baseQuote[1] };
});
prices.sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
);
return {
...state,
prices,
pricesLoading: false,
pricesError: null,
};
}
default:
return state;
}
In Redux, all side-effects (not just api calls) should take place inside Action Creators. You should move this logic into the action creator and have the caller pass the necessary parameters.
I'm playing around with cyclejs and I'm trying to figure out what the idiomatic way to handle many sources/intents is supposed to be. I have a simple cyclejs program below in TypeScript with comments on the most relevant parts.
Are you supposed to model sources/intents as discreet events like you would in Elm or Redux, or are you supposed to be doing something a bit more clever with stream manipulation? I'm having a hard time seeing how you would avoid this event pattern when the application is large.
If this is the right way, wouldn't it just end up being a JS version of Elm with the added complexity of stream management?
import { div, DOMSource, h1, makeDOMDriver, VNode, input } from '#cycle/dom';
import { run } from '#cycle/xstream-run';
import xs, { Stream } from 'xstream';
import SearchBox, { SearchBoxProps } from './SearchBox';
export interface Sources {
DOM: DOMSource;
}
export interface Sinks {
DOM: Stream<VNode>
}
interface Model {
search: string
searchPending: {
[s: string]: boolean
}
}
interface SearchForUser {
type: 'SearchForUser'
}
interface SearchBoxUpdated {
type: 'SearchBoxUpdated',
value: string
}
type Actions = SearchForUser | SearchBoxUpdated;
/**
* Should I be mapping these into discreet events like this?
*/
function intent(domSource: DOMSource): Stream<Actions> {
return xs.merge(
domSource.select('.search-box')
.events('input')
.map((event: Event) => ({
type: 'SearchBoxUpdated',
value: ((event.target as any).value as string)
} as SearchBoxUpdated)),
domSource.select('.search-box')
.events('keypress')
.map(event => event.keyCode === 13)
.filter(result => result === true)
.map(e => ({ type: 'SearchForUser' } as SearchForUser))
)
}
function model(action$: Stream<Actions>): Stream<Model> {
const initialModel: Model = {
search: '',
searchPending: {}
};
/*
* Should I be attempting to handle events like this?
*/
return action$.fold((model, action) => {
switch (action.type) {
case 'SearchForUser':
return model;
case 'SearchBoxUpdated':
return Object.assign({}, model, { search: action.value })
}
}, initialModel)
}
function view(model$: Stream<Model>): Stream<VNode> {
return model$.map(model => {
return div([
h1('Github user search'),
input('.search-box', { value: model.search })
])
})
}
function main(sources: Sources): Sinks {
const action$ = intent(sources.DOM);
const state$ = model(action$);
return {
DOM: view(state$)
};
}
run(main, {
DOM: makeDOMDriver('#main-container')
});
In my opinion you shouldn't be multiplexing intent streams like you do (merging all the intent into a single stream).
Instead, you can try returning multiple streams your intent function.
Something like:
function intent(domSource: DOMSource): SearchBoxIntents {
const input = domSource.select("...");
const updateSearchBox$: Stream<string> = input
.events("input")
.map(/*...*/)
const searchForUser$: Stream<boolean> = input
.events("keypress")
.filter(isEnterKey)
.mapTo(true)
return { updateSearchBox$, searchForUser$ };
}
You can then map those actions to reducers in the model function, merge those reducers and finally fold them
function model({ updateSearchBox$, searchForUser$ }: SearchBoxIntents): Stream<Model> {
const updateSearchBoxReducer$ = updateSearchBox$
.map((value: string) => model => ({ ...model, search: value }))
// v for the moment this stream doesn't update the model, so you can ignore it
const searchForUserReducer$ = searchForUser$
.mapTo(model => model);
return xs.merge(updateSearchBoxReducer$, searchForUserReducer$)
.fold((model, reducer) => reducer(model), initialModel);
}
Multiple advantages to this solution:
you can type the arguments of your function and check that the right stream are passed along;
you don't need a huge switch if the number of actions increases;
you don't need actions identifiers.
In my opinion, multiplexing/demultiplexing streams is good when there is a parent/child relationship between two components. This way, the parent can only consume the events it needs to (this is more of an intuition than a general rule, it would need some more thinking :))
Let's say you have two objects to store in Redux, A and B, which are denormalized per the redux docs, like this:
state = {
a: { byId: { id: { obj } }, allIds: [] },
b: { byId: { id: { obj } }, allIds: [] }
};
You have an action, CREATE_A which adds a new A to the store. But, for every A created, it inherently needs a B as well. Assume that A and B are in separate reducer slices (i.e. combineReducers) and can't be merged into one.
It's easy to make B's reducer react to the CREATE_A event and create a new B, but what if the B object needs the A's ID that was just created?
Even if there's a join table involed to join B to A, there's still the problem of "knowing the A that was created first". The solution I came up with, was to store the last created A like so:
a: { lastCreated: {}, byId: etc, allIds: etc }
And then passing the whole state tree to either B's reducer or the join table reducer, so it can access state.a.lastCreated. It doesn't feel right, though, to just have a key so that a later reducer knows what happened (the idea of reducers requiring a certain ordering also seems wrong)
I thought you could also dispatch a CREATE_B with the new A's id, but that would have to be done in an async action (since you can't dispatch from a reducer), which also doesn't feel right.
In a procedural world, this would be trivial:
a = createA();
createB(a);
But even with two dispatches, I'm not sure how it would work:
dispatch( createA() )
dispatch( createB(???) )
What would be the best way to handle this case of "A-inherently-means-B-as-well"?
Edit: Let me try to use some more concrete examples.
Say you have squares and points. Creating a square inherently means creating 4 points. The points are joined with the square, because they form the square, but they also don't belong to the square, because they can be independent objects of their own right:
So, ADD_SQUARE needs to both add a square, and add 4 points, and then join the two together, and I'm not sure how to do this without writing a reducer directly in the parent "state" like this (I don't want to have to do this, it gets really messy really fast, imagine having to repeat for 3-8 sided polygons):
function reducer(state, action) {
switch(state) {
case ADD_SQUARE:
const points = create4Points();
const square = createSquare();
return {
...state,
squares: {
...state.squares,
byId: {
...state.squares.byId
[square.id]: square
}
},
points: {
...state.points,
byId: {
...state.points.byId,
[point[0].id]: point[0],
[point[1].id]: point[1],
[point[2].id]: point[2],
[point[3].id]: point[3]
}
},
pointsSquares: {
...state.pointsSquares,
byId: {
[square.id]: {
square: square.id,
points: [point1.id, point2.id, point3.id, point4.id]
}
}
}
};
}
}
You could define the same action within both A and B reducers.. like so --
function reducerA(state, action) {
switch(action.type) {
case A_CREATE:
do A stuff
}
}
function reducerB(state, action) {
switch(action.type) {
case A_CREATE:
do B stuff
}
}
Then when you dispatch the action it will affect both reducers.
function createA({id, stuff, etc}) {
return {
type: "A_CREATE",
payload: {id, stuff, etc}
}
}
Then you can tie the id to whatever you need to create... thus "joining" them together.
EDIT:
You could use redux thunk or some other middleware like redux saga in conjunction with promises to dispatch multiple actions.
`
function handleACreation(payload) {
dispatch(
createA(payload)
.then(result => dispatch(updateB(result)))
)
}
`
obviously that exact code wouldn't work - but the general idea remains :] hope this helps!
In my case, I solved this by creating A's id in the action creator instead of in A's reducer. For example,
function createSquare () {
return {
type: CREATE_SQUARE,
id: newUuid()
};
}
function squareReducer ( state, action ) {
switch ( action.type ) {
case CREATE_SQUARE:
return { id: action.id };
}
}
function pointReducer ( state, action ) {
switch ( action.type ) {
case CREATE_SQUARE:
return [
{ id: newUuid(), square: action.id }
{ id: newUuid(), square: action.id } //etc
];
}
}
I'm still open to other solutions though.
As the doc says:
Things you should never do inside a reducer:
Mutate its arguments;
Perform side effects like API calls and routing transitions;
Call non-pure functions, e.g. Date.now() or Math.random().
If I follow the principle, there are some questions about the code orgnization (my app is a file manager).
For example,
default reducer like this:
export default function (state = initialState, action) {
const { path } = action
if (typeof path === 'undefined') {
return state
}
const ret = {
...state,
[path]: parentNode(state[path], action)
};
switch (action.type) {
case OPEN_NODE:
case GO_PATH:
ret['currentPath'] = path
break
default:
break
}
return ret
}
data struct in state[path] likes:
{
'open': false,
'path': '/tmp/some_folder',
'childNodes' : [ {'path':'/some/path', 'mode': '0755', 'isfolder': true}, ....],
'updateTime': Date.now()
}
Now I need several actions such as ADD_CHILD, DELETE_CHILD , RENAME_CHILD, MOVE_CHILD, there are two sulotions(by change state in actions or reducers):
1. All functional code in actions:
actions:
export function updateChildNodes(path, nodes) {
return {
type: UPDATE_CHILD_NODES,
path: path,
loading: false,
loaded: true,
childNodes: nodes,
};
}
export function addChild(path, node) {
return (dispatch, getState) => {
const state = getState().tree[path]
var childNodes = state.childNodes ? state.childNodes :[]
childNodes.push(node)
return dispatch(updateChildNodes(path, childNodes))
}
}
export function deleteChild(parent_path, child_node) {
return (dispatch, getState) => {
const state = getState().tree[parent_path]
var childNodes = state && state.childNodes ? state.childNodes : []
for (var i=0; i <=childNodes.length; i++){
if (childNodes[i].path == child_node.path){
childNodes.splice(i, 1)
return dispatch(updateChildNodes(parent_path, childNodes))
}
}
}
}
export function deleteNode(node) {
return (dispatch, getState) => {
// ajax call
return api.deleteChild(node.path, () => {
dispatch(deleteChild(node.parent, node))
})
}
}
.....
parentNode reducer:
function parentNode(state, action) {
switch (action.type) {
case UPDATE_CHILD_NODES:
return {
...state,
childNodes: action.childNodes
}
default:
return state;
}
}
All variable pass in parentNode from actions, parentNode just assign change to state doesn't do anything else.
All logic of remove node and add node is done by actions, only UPDATE_CHILD_NODES in parentNode.
2. Action just send data to reducer, let reducer to process
actions:
export function updateChildNodes(path, nodes) {
return {
type: UPDATE_CHILD_NODES,
path: path,
loading: false,
loaded: true,
childNodes: nodes,
};
}
export function addChild(path, node) {
return {
type: ADD_CHILD,
path: path,
node: node,
};
}
export function deleteChild(path, node) {
return {
type: DELETE_CHILD,
path: path,
node: node,
};
}
export function deleteNode(node) {
return (dispatch, getState) => {
// ajax call
return api.deleteChild(node.path, () => {
dispatch(deleteChild(node.parent, node))
})
}
}
.....
parentNode reducer:
function parentNode(state, action) {
switch (action.type) {
case DELETE_CHILD:
let childNodes = state.childNodes.slice() // have to clone obj
for (var i=0; i <=childNodes.length; i++){
if (childNodes[i].path == action.node.path){
childNodes.splice(i, 1)
}
}
return {
...state,
childNodes: childNodes
};
case ADD_CHILD:
let childNodes = state.childNodes.slice() // have to clone obj
childNodes.push(node)
return {
...state,
childNodes: childNodes
};
case UPDATE_CHILD_NODES:
return {
...state,
childNodes: action.childNodes
}
default:
return state;
}
}
In my option, the solution 2 is more readable and pretty.
But is it good to change the state by mutate an cloned obj? And when I need set updateTime by Date.now(), I have to generate it from actions and pass to reducer,so that state variables are generated in different place(But I'd like put them together...)
Any opinion for this?
From this redux discussion here:
It is best practice to place most of the logic in the action creators and leave the reducers as simple as possible (closer to your option 1)
for the following reasons:
Business logic belongs in action-creators. Reducers should be stupid and simple. In many individual cases it does not matter- but consistency is good and so it's best to consistently do this. There are a couple of reasons why:
Action-creators can be asynchronous through the use of middleware like redux-thunk. Since your application will often require asynchronous updates to your store- some "business logic" will end up in your actions.
Action-creators (more accurately the thunks they return) can use shared selectors because they have access to the complete state. Reducers cannot because they only have access to their node.
Using redux-thunk, a single action-creator can dispatch multiple actions- which makes complicated state updates simpler and encourages better code reuse.
For small apps I usually put my logic in action creators. For more complex situations you may need to consider other options. Here is a summary on pros and cons of different approaches: https://medium.com/#jeffbski/where-do-i-put-my-business-logic-in-a-react-redux-application-9253ef91ce1#.k8zh31ng5
Also, have a look at Redux middleware.
The middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
This is an answer provided by Dan Abramov (author of Redux): Why do we need middleware for async flow in Redux?
And here are the official Redux docs: http://redux.js.org/docs/advanced/Middleware.html
Is this a reasonable solution for data sharing between two states/reducers?
//combineReducers
function coreReducer(state = {}, action){
let filtersState = filters(state.filters, action);
let eventsState = events(state.events, action, { filters: filtersState});
return { events: eventsState, filters : filtersState};
}
export const rootReducer = combineReducers(
{
core : coreReducer,
users
}
);
If so, how can one guarantee the order in which reducer functions are executed if both answer to the same dispatched event and the second reducing function depends on the new state of the first one?
Let's say that we dispatch a SET_FILTER event that appends to activeFilters collection in the filters Store and later changes the visibility of items in the events Store with respect to the activeFilters values.
//ActiveFilters reducer
function filtersActions(state = {}, action){
switch (action.type) {
case SET_FILTER:
return Object.assign({}, state, {
[action.filterType]: action.filter
})
case REMOVE_FILTER:
var temp = Object.assign({}, state);
delete temp[action.filterType];
return temp;
case REMOVE_ALL_FILTERS:
return {};
default:
return state
}
}
I think I found the answer - Computing Derived Data - Reselect
http://redux.js.org/docs/recipes/ComputingDerivedData.html
/--------container--------/
import {getGroupsAndMembers} from '../reducers'
const mapStateToProps = (state) => {
return {
inputValue: state.router.location.pathname.substring(1),
initialState: getGroupsAndMembers(state) <-- this one
}
}
/--------reducers--------/
export function getGroupsAndMembers(state){
let { groups, members } = JSON.parse(state)
response = {groups, members}
return response;
}
GroupsContainer.propTypes = {
//React Redux injection
pushState: PropTypes.func.isRequired,
// Injected by React Router
children: PropTypes.node,
initialState:PropTypes.object,
}
don't forget to follow the guidelines for 'connect'
export default connect(mapStateToProps,{ pushState })(GroupsContainer)
If you have two reducers, and one depend on a value from a first one, you just have to update them carefully, and the best solution will be just to use a special function, which will first set the filtering, and then query corresponding events. Also, keep in mind that if events fetching is asynchronous operation, you should also nest based on filtering type -- otherwise there is a chance of race condition, and you will have wrong events.
I have created a library redux-tiles to deal with verbosity of raw redux, so I will use it in this example:
import { createSyncTile, createTile } from 'redux-tiles';
const filtering = createSyncTile({
type: ['ui', 'filtering'],
fn: ({ params }) => params.type,
});
const events = createTile({
type: ['api', 'events'],
fn: ({ api, params }) => api.get('/events', { type: params.type }),
nesting: ({ type }) => [type],
});
// this function will just fetch events, but we will connect to apiEvents
// and filter by type
const fetchEvents = createTile({
type: ['api', 'fetchEvents'],
fn: ({ selectors, getState, dispatch, actions }) => {
const type = selectors.ui.filtering(getState());
return dispatch(actions.api.events({ type }));
},
});