My mapStateToProps function
const mapStateToProps = (state, props) => ({
object: {},
criteria: []
});
It contains two properties:
1. The object property
It get's an object from the store corresponding the URL's object id.
The resulting object looks like this:
{
object: {
id: 42,
criteria: [1, 2, 3]
}
}
2. The criteria property
This one should fetch the criteria objects from the store, with the id's of the previously fetched object's criteria ids.
I came up with the following function:
const mapStateToProps = (state, props) => ({
object: getObjectById(state, props.routeParams.objectId),
criteria: getCriteriaByIds(state, getObjectById(state, ownProps.routeParams.objectId) ? getObjectById(state, ownProps.routeParams.objectId).criteria : []),
});
The problem
There is a lot of repetition going on here. If I use criteria: getCriteriaByIds(state.props.object.criteria) directly, it seems that the object is equal to the previous state object (and thus the object can be undefined).
I'm sure there must be a simpler way to fetch the criteria without too much repetition in the code.
Note
The getObjectById and getCriteriaByIds are selectors based on the Colocating selectors with reducers course on Egghead.io.
So my question is
What is the recommended way to work with dependencies on other state
properties when working with mapStateToProps?
I don't have anything useful to add about "the recommended way to work with dependencies on other state properties when working with mapStateToProps", but I think just adding a local variable will help with the repetition a bit.
const mapStateToProps = (state, props) => {
let object = getObjectById(state, props.routeParams.objectId);
return {
object,
criteria: getCriteriaByIds(state, object ? object.criteria : [])
};
};
If you don't expect object to be undefined this could be further simplified to
const mapStateToProps = (state, props) => {
let object = getObjectById(state, props.routeParams.objectId);
return {
object,
criteria: getCriteriaByIds(state, object.criteria || [])
};
};
I would suggest not to do any kind of selection logic in mapStateToProps. That function is called EVERY time any state is changed, so your logic (getCriteriaByIds) will be called a lot.
I suggest to have objectId as top level state entry (possible with standalone reducer), so that you can just take it from state object. Consider same for critetia.
My suggestion is backed up by best practices to keep your Redux state as flat (normalized) as possible, even if you will have some repetition there
Related
I have an object variable within the state of my react app. I initialize it with the structure that I want the object to have. Late I am trying to update that object using the setState function. My issue is, I can't get it to actually update anything within the state. The way my code currently looks is:
// the initial state
var initialState = {
object: {
foo: '',
bar: 0,
array: [<an array of objects>]
}
};
// in componentDidMount
this.state = initialState;
// updating the object that is within the state (also in componentDidMount)
let object = {
foo: 'Value',
bar: 90,
array: [<a populated array of objects>]
};
this.setState(prevState => ({
...prevState.object,
...object
}));
console.log(this.state.object); // returns the object defined in initial state
I am really not sure how to fix this issue. I have also been trying a few other methods (especially the ones outlined in this post: Updating an object with setState in React
I have not tried every method outlined in that post but the ones I have been trying have not been working and I figure that this means it is a mistake that does not involve which method I am using. Any insight into this issue would be greatly appreciated. I tried to make this code as concise as possible but if you want to see the exact code I am using (which is pretty much just this but more) just ask.
Edit 2
You have to care each object key equality.
You can do this.
this.setState((prevState) => {
console.log('logging state: ', prevState)
return {
...prevState.object,
object:{ ...object }
})
)}
In my create-react-app app, I created an object using the useState hook in App.js that I pass down as an argument to 5 different instances of the same component. This object contains 5 different arrays under simple number property names (so they're easy to iterate over with a for loop).
const [ listObj, setListObj ] = useState({ 0: [], 1: [], 2: [], 3: [], 4: [] });
Each component maintains and modifys one and only one of those arrays (corresponding to it's number, so component 0 only modifies component 0, 1 only modifies 1, etc.), and I use functions to iterate over listObj and the arrays inside so I can compare their contents.
These components each have a unique componentNumber (0-4) passed to them through App.js, and a useEffect hook that looks something like this in each one for every time they want to update their specific array:
useEffect(() => {
let newArray = functionToGenerateSortedArrays();
let newListObj = { ...listObj, [ componentNumber ]: newArray };
setListObj( newListObj );
}, [ (all my state objects that trigger useEffect) ])
This works great for when I'm only updating something pertaining to one component at a time. The problem is that there is a functionality in the app where all the components need to update at the same time. It appears that each one will grab an "old" copy of the listObj, use that to create a newListObj with the updated information, then call setListObj with the newListObj, thereby overwriting the changes that the other components tried to make. The only listObj array that updates properly is the last one in the row.
I thought myself terribly clever when I tried to implement this hacky solution:
useEffect(() => {
let newArray = functionToGenerateSortedArrays();
setTimeout(() => {
let newListObj = { ...listObj, [ componentNumber ]: newArray };
setListObj( newListObj );
}, componentNumber * 100 );
}, [ (all my state objects that trigger useEffect) ])
This doesn't work, though. It actually switches the array in listObj back to the value I want instantly, but it reverts to the wrong one after the timeout triggers. I have no idea why but would love to understand it if someone has any insights. I thought perhaps it could be related to how React "batches" state updates, but couldn't find any information on how to keep React from doing that so I could test my hypothesis.
Do I have any options for changing the array values in listObj in a "cascading" manner so they don't interfere with each other? Is there perhaps another way of saving and updating state that won't create this problem? Thanks very much for your time.
This is where to us the state update callback form of the useState hook's setter:
useEffect(() => {
let newArray = functionToGenerateSortedArrays();
setListObj(currentListObj => {
// ^^^^^^^^^^^^^^
let newListObj = { ...currentListObj, [ componentNumber ]: newArray };
return newListObj;
});
}, [ (all my state objects that trigger useEffect) ])
That way, even when all the update functions are batched together, they will receive the current value instead of using the outdated one that the effect function had closed over.
I know my question comes from a misunderstanding of react-redux but I will describe my use case hoping someone will point me in the right direction.
I'm trying to store selected row keys (selectedRowKeys) from a table (ant design table) inside of a redux store. Everything works when the store structure is simple like this:
selectedRowKeys: []
But I want to store that state in a normalized form to handle multiple tables and multiple table properities:
tables: {
123fdfas-234dasf-234asdf-23rfa : { //table id
id: 123fdfas-234dasf-234asdf-23rfa,
selectedRowKesy: []
//... other properities
}
}
The problem is that this state doesn't exist when redux is trying to mapStateToProps like this:
const mapStateToProps = (state, ownProps) => {
if (!ownProps.id) {
ownProps.id = uuidv4();
}
return {
selectedRowKeys: state.tables[ownProps.id].selectRowKeys
};
};
state.tables[ownProps.id] is undefined so there is an error.
I thought that I need to initialize the state somehow but this led me to even more confusion. What I have figured out so far:
I can't initialize state in reducer like reducerName (state = initialState, action) because action is not dispatched and there is no action.id (action object has a payload with table id).
I can't dispatch an action INIT_TABLE in componentDidMount() because mapStateToProps executes first so state.tables[ownProps.id] is still undefined.
I feel like this use case is wierd and that is way I cannot find the solution although I have been googling and thinking about this for 3 days.
Please guide me, I'm in a crazy loop :)
This is my first SO question, pls let me know if something is unclear.
Who is responsible for creating a new table? That's the deciding factor on how to solve this problem.
The id definitely shouldnt be created in mapStateToProps and you shouldn't mutate ownProps. I'm surprised if that even works. Id should be created in action if using redux.
If your React-app has some mechanism which creates a new table (for example, user clicks button), then that's where you should dispatch initialization action. If you really can't find parent component which would be responsible for table initialization, then maybe it is responsibility of this component and you should dispatch the action in componentDidMount.
Regardless of which option you pick, your mapStateToProps should handle empty state gracefully ie. selectedRowKeys should be set to some default value if it's missing (empty array or null maybe?). And your component should handle missing values if there is no sane default available. It's common to have some null checks in render-function which return null until data is available.
From my understanding to the problem, you are being too specific!
In this case, it's not a good practice.
I advice the follow.
Supposing that you have the reducer tables:
const mapStateToProps = ({ tables }, ownProps) => {
return {
tables,
};
};
This will maket the reducers tables available at your component's this.props
Thus, you can do the follow wherever you want in the component to get your selected rows, define this method and use it in a proper place instead of directly doing it in mapStateToProps, which is not a good practice.
getSelectedRowKeys = (ownProps) => {
if (!ownProps.id) {
ownProps.id = uuidv4();
}
const selectedRowKeys = [];
const table = this.props.tables[ownProps.id];
const selectedRowKeys = table && table.selectRowKeys; //if table exists get the row keys.
return selectedRowKeys || []; //if selectedRowKeys are not there return an empty array.
}
According to the redux FAQ from here https://github.com/reduxjs/redux/blob/master/docs/recipes/UsingImmutableJS.md#what-are-some-opinionated-best-practices-for-using-immutable-js-with-redux:
"Your selectors should return Immutable.JS objects". "Always".
Why is this the case?
As a disclaimer, this isn't "always" the case, but the docs are trying to point you in the right direction for most cases.
Since reselect memoizes the return result of selectors, returning a mutable object leaves you susceptible to tricky bugs. Imagine the following scenario:
// Immutable State
{
todos: [{ text: "hey"}, { todo: "text"}]
}
// Selectors
const getTodos = createSelector(state => state.todos, immutableTodos => immutableTodos.toJS())
The getTodos selector is returning a plain JS object, which by default is mutable. Now imagine multiple smart components that are using the getTodos selector.
class EditTodos extends PureComponent {
constructor(props) {
this.state = { todos: props.todos }
}
addUnsavedTodo(newTodo) {
// Accidentally mutate the return result of getTodos
const newTodos = this.state.todos.push(newTodo)
this.setState({ todos: newTodos })
}
render() { // Some components for adding unsaved todos }
}
const mapStateToProps = (state) => ({ todos: getTodos(state))
A second component also using getTodos would see the new "unsaved" todo as soon as addUnsavedTodo is called, which would most likely be unintentional. All calls to getTodos, assuming redux state is unchanged, will get the same reference and any mutations will affect all consumers.
The example above is contrived, but hopefully it demonstrates one of the reasons that returning plain JS objects can be risky.
Furthermore, as the docs mention, you should limit your use of toJS since it has performance implications. There is no benefit to converting your immutable object into a plain JS object within a selector
I'm currently using React with Redux and when I run the following:
const mapStateToProps = state => {
const employees = _.map(state.employees, (val, uid) => {
return { ...val, uid };
});
I get the error stated above which seems to be targeting "...val" in the return method. I'm basically trying to pull all the information I had from Firebase and returning it in the variable "employees." Any help would be appreciated, thanks!
I think the issue is the typeof val isn't an object (either an array[] or an object {}). You can just use return {val, uid}