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 }));
},
});
Related
I have influencer data object. This object is beeing pulled from database with action FETCH_INFLUENCER and put inside two different objects: influencer and formInfluencer in redux store. And then I have action SET_INFLUENCER that is supposed to create new instance of the state and update influencer object in redux. For some reason though it updates both influencer and formInfluencer. I really struggle with finding answer here since I think I did everything to prevent pointing of two different variables to the same object and still it happens.
reducer:
case 'FETCH_INFLUENCER_FULFILLED':
return { ...state, fetching: false, fetched: true, influencer: action.payload.data, formInfluencer: Object.assign([], action.payload.data) }
case 'SET_INFLUENCER':
return { ...state, influencer: action.payload }
actions:
export function fetchInfluencer(id) {
return {
type: "FETCH_INFLUENCER",
payload: axios.get('/api/influencer/' + id, {headers: {Authorization: 'Bearer ' + localStorage.getItem('token')}})
}
}
export function setInfluencer(influencer) {
return {
type: "SET_INFLUENCER",
payload: influencer
}
}
dispatch:
handleUserChange(e) {
let influencer = [...this.props.influencer]
influencer[0].user[e.target.name] = e.target.value;
this.props.dispatch(setInfluencer(influencer))
}
mapping state to props:
const mapStateToProps = (state) => {
return {
influencer: state.influencers.influencer,
formInfluencer: state.influencers.formInfluencer
}
}
export default connect(mapStateToProps)(InfluencerDetails)
If You have any idea why this could be happening I would be happy to hear the answer.
You shouldn't mutate state (if you don't mutate it, then it is no problem that you have multiple variables pointing to the same object).
Instead of:
handleUserChange(e) {
let influencer = [...this.props.influencer]
influencer[0].user[e.target.name] = e.target.value;
this.props.dispatch(setInfluencer(influencer))
}
You should do a bit more work:
handleUserChange(e) {
const newUser = {
...this.props.influencer[0].user,
[e.target.name]: e.target.value
};
const newInfluencer = {
...this.props.influencer[0],
user: newUser
};
const newInfluencers = [...this.props.influencer];
newInfluencers[0] = newInfluencer;
this.props.dispatch(setInfluencer(newInfluencers));
}
I am trying to test a React component which uses one of the overloads for setState, but am unsure how to assert the call correctly. An example component would be:
class CounterComponent extends React.Component {
updateCounter() {
this.setState((state) => {
return {
counterValue: state.counterValue + 1
};
});
}
}
The assumption here is that this method will be called asyncronously, so cannot rely on the current state, outwith the call to setState (as it may change before setState executes). Can anyone suggest how you would assert this call? The following test fails as it is simply comparing the function names.
it("Should call setState with the expected parameters", () => {
const component = new CounterComponent();
component.setState = jest.fn(() => {});
component.state = { counterValue: 10 };
component.updateCounter();
const anonymous = (state) => {
return {
counterValue: state.counterValue + 1
};
};
//expect(component.setState).toHaveBeenCalledWith({ counterValue: 11 });
expect(component.setState).toHaveBeenCalledWith(anonymous);
});
Edit: Given yohai's response below, i will add some further context as I feel i may have over simplified the problem however i do not want to re-write the entire question for clarity.
In my actual component, the state value being edited is not a simple number, it is an array of objects with the structure:
{ isSaving: false, hasError: false, errorMessage: ''}
and a few other properties. When the user clicks save, an async action is fired for each item in the array, and then the corresponding entry is updated when that action returns or is rejected. As an example, the save method would look like this:
onSave() {
const { myItems } = this.state;
myItems.forEach(item => {
api.DoStuff(item)
.then(response => this.handleSuccess(response, item))
.catch(error => this.handleError(error, item));
});
}
The handle success and error methods just update the object and call replaceItem:
handleSuccess(response, item) {
const updated = Object.assign({}, item, { hasSaved: true });
this.replaceItem(updated);
}
handleError(error, item) {
const updated = Object.assign({}, item, { hasError: true });
this.replaceItem(updated);
}
And replaceItem then replaces the item in the array:
replaceItem(updatedItem) {
this.setState((state) => {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
return {
myItems: working
};
});
}
replaceItem is the method I am trying to test, and am trying to validate that it calls setState with the correct overload and a function which correctly updated the state.
My answer below details how I have solved this for myself,but comments and answers are welcome =)
#Vallerii: Testing the resulting state does seem a simpler way, however if i do, there is no way for the test to know that the method is not doing this:
replaceItem(updatedItem) {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
this.setState({ myItems: working });
}
When replaceItem does not use the correct overload for setState, this code fails when called repeatedly as (I assume) react is batching updates and the state this version uses is stale.
I think you should test something a little bit different and it will look somthing like this (I'm using enzyme):
import React from 'react'
import { mount } from 'enzyme'
import CounterComponent from './CounterComponent'
it("Should increase state by one", () => {
const component = mount(<CounterComponent />)
const counter = 10;
component.setState({ counter });
component.instance().updateCounter();
expect(component.state().counter).toEqual(counter + 1);
});
I have come up with a solution to this after some further thought. I am not sure it is the best solution, but given that the updateCounter method in the example above passes a function into the setState call, I can simply get a reference to that function, execute it with a known state and check the return value is correct.
The resulting test looks like this:
it("Should call setState with the expected parameters", () => {
let updateStateFunction = null;
const component = new CounterComponent();
component.setState = jest.fn((func) => { updateStateFunction = func;});
component.updateCounter();
const originalState = { counterValue: 10 };
const expectedState = { counterValue: 11};
expect(component.setState).toHaveBeenCalled();
expect(updateStateFunction(originalState)).toEqual(expectedState);
});
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 :))
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