I have an initial state which looks like this (simplified for the purpose of this question):
export default {
anObject: {
parameters: {
param1:'Foo',
param2:'Bar'
},
someOtherProperty:'value'
}
};
And I have a reducer for anObject part of which deals with changes to parameter. I have an action which passed the id of the parameter to change, along with the newValue for that parameter. The reducer (again, very slightly simplified) looks like this:
import * as types from '../actions/actionTypes';
import initialState from './initialState';
export default function anObjectReducer(state = initialState.anObject, action){
switch(action.type){
case types.UPDATE_PARAMETER:
return Object.assign(
{},
state,
{
parameters:Object.assign(
{},
state.parameters,
{ [action.id]: action.newValue })
});
default:
return state;
}
}
This reducer looks wrong to me. I assumed I would be able to just do it like this:
case types.UPDATE_PARAMETER:
return Object.assign({},state,{parameters:{[action.id]:action.newValue}});
But this seems to wipe out all the other parameters and just update the single one being changed. Am I missing something obvious about how to structure my reducer?
In case it's relevant this is how I set up my root reducer:
import { combineReducers } from 'redux';
import anObject from './anObjectReducer';
export default combineReducers({
anObject
});
I thought there might be a way to compose reducers for the individual parts of each object - ie separately for parameters and someOtherProperty part of anObject in my example?
The reason why it wipes out other parameters is because you don't pass the previous values in the Object.assign.
You should have done that:
return Object.assign({}, state, {
parameters: Object.assign({}, { [action.id]: action.newValue }),
});
Or with the ES6 spread syntax: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator
return {
...state,
parameters: {
...state.parameters,
[action.id]: action.newValue,
}
}
You can:
restructure your reducers: you may use combineReducers not for store's root only. This way store stays the same as well as actions but reducers for nested object become lightweight
restructure your state(and reducers) to have it as flat as possible. it'd be more efforts needed but typically it also make easier to fetch data in mapStateToProps. normalizr should help to make transition easier by encapsulating integration with existing API that
requires specific data structure
use immer to write code like you're mutating state. This is definitely bad idea if you are learning React, but I'd consider using it on small real projects
Related
I'm learning composition API doing a simple todo app but then with multiple swim-lanes (todo, doing, done).
in useTasks.js composable I create "global" sourceTasks reactive, that later gets filled with data pulled from an API. Then it is reduced in tasks computed property, like this:
// useTasks.js
const sourceTasks = reactive({
list: []
});
export default function useTasks() {
const tasks = computed(() => {
return sourceTasks.list.reduce(divideIntoSwimLanes, [])
});
...
return {
tasks,
loadTasks,
createTask
}
}
Nothing too complicated.
Then I've got this SwimLane component, that well... uses the tasks :)
// SwimLane.vue - setup
async setup(props) {
const { status } = toRefs(props);
const { tasks, createTask } = useTasks();
return {
tasks,
label,
createTask
}
}
// SwimLane.vue - template
<single-task class="m-3" v-for="task in tasks[status]" :title="task.title" :id="task.id"/>
This works, but I don't find it elegant. I would prefer to create a new computed inside of SwimLane's setup, that holds the value of tasks for the given SwimLane. Putting it in the template obscures the logic.
I would expect this to work, but it does not, I think it loses the reactivity but I cant wrap my head around why:
// SwimLane.vue - alternative setup
const currentContextTasks = computed(() => {
return tasks.value[status]
});
return {
currentContextTasks
}
The problem feels a bit ridiculous, but my main concern is that I have misunderstood some core concept, hence this lengthy post.
This is like a biggest blunders ever. So right, the post was really helpful as a form of talking to the rubber duck.
What I did forgot to do is use the value of the status. Since it is a ref, I had to use it as follows:
const currentContextTasks = computed(() => {
return tasks.value[status.value] // 'status.value' and not just 'status'
});
What way is better? I think than first way is better. Import parent in child looks weird to me, but maybe I'm wrong.
RootStore:
export const RootStore = types
.model('RootStore', {
store1: types.optional(Store1, {}),
store2: types.optional(Store2, {}),
store3: types.optional(Store3, {}),
store3: types.optional(Store4, {}),
name: 'name'
})
export const rootStore = RootStore.create()
First way:
export const Store1 = types
.model('Store1', {
some: ''
})
.views(self => ({
get rootStore() {
return getParent(self)
},
get name() {
return self.rootStore.name
}
}))
Second way:
import { rootStore } from './rootStore'
export const Store1 = types
.model('Store1', {
some: ''
})
.views(self => ({
get name() {
return rootStore.name
}
}))
All answers to this question are likely to be opinionated..
If you are going to do this, I think the first way is better. Just because it means the child does not need to know anything about it's parent other than that it exposes a name property.
That being said, I'm really not a big fan of either approaches.
Whether you use getParent or a closure, this encourages a coupling of the two models. This results in decreased modularity and harder testing since every Store1 must be a child of a RootStore to function properly.
I think a better approach would be to remove the dependency between child->parent. However, if you are purposefully making use of the tree structure that MST provides, my suggestion might be better in theory than practice.
The simplest approach to removing the dependency is to have the caller of Store1's actions/views pass whatever data is needed in as parameters. Once again, this does not always make sense in practice.
If all you need is access the root node in the tree, then there's a dedicated helper function specifically for that case - getRoot(self).
Given an object in a model tree, returns the root object of that tree.
So far I have been following various tutorials. This time I'm trying to build things from scratch (kind of). For now the following is supposed to display part of state. Later on I'll play with making it do calculations,etc. Still I get an error:
Cannot read property 'count' of undefined
So I use mapStateToProps and the first step I'd like to do is to get it to display this.props.count and this.props.step. Once I've done it I'll modify it to do more complex things.
Here's the component and below there's a link to the whole code that I put on github.
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { connect } from 'react-redux';
import { getCounter } from '../actions';
class CounterBoard extends Component {
render() {
return (
<View>
<Text>BELOW SHOULD DISPLAY 0</Text>
<Text>{this.prop.count}</Text>
</View>
);
}
}
const mapStateToProps = (state) => {
return {
count: state.count,
step: state.step
};
};
export default connect(mapStateToProps, { getCounter })(CounterBoard);
https://github.com/wastelandtime/calculator
Edit: Thank you for the 'prop' => 'props' pointer. Now I have the following error:
ExceptionsManager.js:63 Objects are not valid as a React child (found: object with keys {count, step}). If you meant to render a collection of children, use an array instead or wrap the object using createFragment(object) from the React add-ons. Check the render method of `Text`.
After going through your code on Github, I couldn't help but notice that in your action, you return the following object:
export const getCounter = (count) => {
return {
type: GET_COUNTER,
payload: count
};
};
If you intend to return the count, I assume that action.payload should contain a number here and not an object.
However, in your reducer, you return:
case GET_COUNTER:
return action.payload.count;
Assuming from the code in your CounterBoard Component and CalcReducer, you probably wanted to merge your payload into the initial state before returning from the reducer?
You might also need to pass and argument while dispatching the getCounter action, for the component to work as expected.
I'm dealing with a big json with a lot of editable values (*big means > 1000), entirely rendered on the same page, so my state is simply { data: bigBigJson }.
The initial rendering is quite long but it's ok.
The problem is that when an input triggers an onChange (and a redux action), the value is updated in the state, and the whole rendering happens again.
I wonder how people deal with that? Is there simple solutions (even not necessarily best practices).
Notes:
The json document is provided by an external API, I can't change it
I could separate the state in several sub-states (it's a multiple levels json), but hoping for a simpler/faster solution (I know it would probably be a best practice though)
I'm using react and redux, not immutable.js but everything is immutable (obviously)
––
Update (about DSS answer)
• (Case 1) Let's say the state is:
{
data: {
key1: value1,
// ...
key1000: value1000
}
}
If keyN is updated, all the state would be re-rendered anyway right? The reducer would return something like:
{
data: {
...state.data,
keyN: newValueN
}
That's one thing but it's not really my case.
• (Case 2) The state is more like (over simplified):
{
data: {
dataSet1: {
key1: value1,
// ...
key10: value1000
},
// ...
dataSet100: {
key1: value1,
// ...
key10: value1000
}
}
}
If dataN.keyN is updated, I would return in the reducer
{
data: {
...state.data,
dataN: {
...state.data.dataN,
keyN: newValueN
}
}
}
I guess i'm doing something wrong as it doesn't look really nice.
Would it change anything like that:
// state
{
dataSet1: {
key1: value1,
// ...
key10: value1000
},
// ...
dataSet100: {
key1: value1,
// ...
key10: value1000
}
}
// reducer
{
...state,
dataN: {
...state.dataN,
keyN: newValueN
}
}
Finally, just to be more specific about my case, here is more what my reducer looks like (still a bit simplified):
import get from 'lodash/fp/get'
import set from 'lodash/fp/set'
// ...
// reducer:
// path = 'values[3].values[4].values[0]'
return {
data: set(path, {
...get(path, state.data),
value: newValue
}, state.data)
}
• In case you are wondering, i can't just use:
data: set(path + '.value', newValue, state.data)
as other properties needs to be updated as well.
The reason everything gets rerendered is because everything in your store changes. It may look the same. All properties may have the same values. But all object references have changed. That is to say that even if two objects have the same properties, they still have separate identities.
Since React-Redux uses object identity to figure out if an object has changed, you should always make sure to use the same object reference whenever an object has not changed. Since Redux state must be immutable, using the old object in the new state is a guaranteed not to cause problems. Immutable objects can be reused in the same way an integer or a string can be reused.
To solve your dilemma, you can, in your reducer, go over the JSON and the store state sub objects and compare them. If they are the same, make sure to use the store object. By reusing the same object React-Redux will make sure the components that represent those objects will not be rerendered. This means that if only one of those 1000 objects changes, only one component will update.
Also make sure to use the React key property correctly. Each of those 1000 items needs its own ID that stays the same from JSON to JSON.
Finally, consider making your state itself more amenable to such updates. You could transform the JSON when loading and updating the state. You could store the items keyed by ID for instance which would make the update process a lot faster.
I want to make requests to two different APIs. I then need to organize that data. I'm using redux-promise.
Currently, I have a function, calls two other functions that do the AJAX request:
export function fetchEstimates(input) {
let firstRequest = fetchFirstRequest(input);
let secondRequest = fetchFirstRequest(input);
return {
type: FETCH_DATA,
payload: {
firstRequest: firstRequest
secondRequest: secondRequest
}
}
}
Unfortunately, by putting both requests in an object, I can't seem to access the results.
export default function(state = [], action) {
switch (action.type) {
case FETCH_DATA:
// console.log(action.firstRequest);
// console.log(action.secondRequest);
return result;
}
return state;
}
As I toggle the object in dev tools, I come to this:
[[PromiseStatus]]:"resolved"
[[PromiseValue]]:Object
I can continue to toggle the options, but I can't seem to access them in my code.
If in my payload, I just return this
payload: firstRequest
I don't have issues. But of course, I need both requests. Any ideas. What is a good approach to handle this?
If you look at the source for redux-promise, you'll see that it assumes you've provided a promise as the "payload" field in your action. You're instead providing an object with two promises as sub-fields under "payload". I'm assuming that you're really interested in having both promises resolve, and then passing both results to the reducer. You'd want to use Promise.all to create a new promise that receives the results of both original promises as an argument, then use that promise as your payload. The reducer would then receive something like: {type : "DATA_RECEIVED", payload : [response1, response2]}.
You need some sort of middleware to deal with Promises (like redux-thunk), and wrap the promises in Promise.all to wait until they're both resolved. Let me know if you need a code example.