My instinct tells me no, but I'm having difficultly thinking of a better way.
Currently, I have a component that displays a list of items. Depending on the provided props, this list may change (i.e. filtering change or contextual change)
For example, given a new this.props.type, the state will be updated as follows:
componentWillReceiveProps(nextProps) {
if (nextProps.type == this.state.filters.type) return
this.setState({
filters: {
...this.state.filters,
type: nextProps.type,
},
items: ItemsStore.getItems().filter(item => item.type == nextProps.type)
})
}
This is all fine and good, but now my requirements have changed and I need to add a new filter. For the new filter, I must execute an API call to return a list of valid item ids and I only want to display items with these ids in the same list component. How should I go about this?
I had thought about calling the appropriate action from componentWillReceiveProps, but that doesn't seem right.
componentWillReceiveProps(nextProps) {
if (nextProps.type == this.state.filters.type && nextProps.otherFilter == this.state.filters.otherFilter) return
if (nextProps.otherFilter != this.state.filters.otherFilter) {
ItemsActions.getValidIdsForOtherFilter(nextProps.otherFilter)
// items will be properly updated in store change listener, onStoreChange below
}
this.setState({
filters: {
...this.state.filters,
type: nextProps.type,
otherFilter: nextProps.otherFilter,
},
items: ItemsStore.getItems().filter(item => item.type == nextProps.type)
})
},
onStoreChange() {
let validIds = ItemsStore.getValidIds()
this.setState({
items: ItemsStore.getItems().filter(item => item.type == this.state.filters.type && validIds.indexOf(item.id) > -1)
})
}
Update 22nd January 2018:
Recently an RFC-PR for React was merged, which deprecates componentWillReceiveProps as it can be unsave when used in the upcoming async rendering mode. An example for this can be calling flux actions from this lifecycle hook.
The correct place to call actions (i.e. side effects) is after React is done rendering, so that means either componentDidMount or componentDidUpdate.
If the intention of the action is to fetch data, React might support a new strategy for these things in the future. In the meantime it's safe to stick to the two mentioned lifecycle hooks.
Related
In my mongoDB I have documents with nested objects that corresponds to which make, model and year of the motorbike they fit to.
Example:
fits: {
honda: {
crf250: {
1990: true,
1991: true
},
rx400: {
2000: true
}
},
kawasaki: {
ninja: {
2015: true
}
}
}
I need to loop through all the makes that the document stores in fits field (In the example above it would be honda and kawasaki) and than return all the models that exist under the specific make. I am succesfully receiving the array of all the models under the make in my aggregate method.
return(
<ul style={{listStyleType: 'none'}}>
{Object.keys(props.data.fits).map((make, i) => {
if(db !== null && client !== null){
var query = `fits.${make}`;
var pipeline = [
{
$match: {
[query]: {
'$exists': true,
'$ne': {}
}
},
},
{
$group: {
_id: `$${query}`,
}
}
]
client.auth.loginWithCredential(new AnonymousCredential()).then((user) => {
db.collection('products').aggregate(pipeline).toArray().then((models)=>{
return <Make
style={{border: '1px solid grey'}}
mongoClient={props.mongoClient}
id={props.data._id}
key={i}
make={make}
data={props.data}
_handleDeleteMake={handleDeleteMake}
_updateRowData={props._updateRowData}
_models={models}
>
</Make>
}).catch(e=>console.log(e))
})
}
})}
</ul>
)
However after the call I need to render the makes. It should look something like this:
Next to the orange plus I want to show the list of all the other models that exists under the specific make so I don't have to repeat in writing the model again if its exists already and I can just click on it.
However rendering Make inside the async I am left with blank:
Now from what I understand is that the render finished before the async function finished that is why it simply renders empty list, but I don't really know how should I approach this problem. Any suggestions?
I don't think it's possible for you to render a React element in that async way. When React try to render your element that is inside the ul tags, because you are using async, at the time of DOM painting, there is nothing for React to render. Thus React render blank.
After the async is resolved, React won't re-render because React doesn't know that there is a new element being added in. Thus even when you actually have that element, since React doesn't re-render, you won't see that element in the app
Why does this happen? Because React only re-render when there are certain "signal" that tells React to re-render. Such is state change, props change, hooks call, etc. What you did doesn't fall into any of those categories, so React won't re-render. This is the same reason why you don't directly change the component state and instead must use method like setState to change it.
I've been working on a SPA for a while and managing my global state with a custom context API, but it's been causing headaches with undesired rerenders down the tree so I thought I'd give react-easy-state a try. So far it's been great, but I'm starting to run into some issues which I assume has to do with the mutability of the global state, something which was easily solved with the custom context api implementation using a lib like immer.
Here's a simplified version of the issue I'm running into: I have a global state for managing orders. The order object primaryOrder has an array of addons into which additional items are added to the order - the list of available addons is stored in a separate store that is responsible for fetching the list from my API. The orderStore looks something like this:
const orderStore = store({
initialized: false,
isVisible: false,
primaryOrder: {
addons: [],
}
})
When a user selects to increases the quantity of an addon item, it's added to the addons array if it isn't already present, and if it is the qty prop of the addon is increased. The same logic applies when the quantity is reduced, except if it reaches 0 then the addon is removed from the array. This is done using the following methods on the orderStore:
const orderStore = store({
initialized: false,
isVisible: false,
primaryOrder: {
addons: [],
},
get orderAddons() {
return orderStore.primaryOrder.addons;
},
increaseAddonItemQty(item) {
let index = orderStore.primaryOrder.addons.findIndex(
(i) => i.id === item.id
);
if (index === -1) {
let updatedItem = {
...item,
qty: 1,
};
orderStore.primaryOrder.addons = [
...orderStore.primaryOrder.addons,
updatedItem,
];
} else {
orderStore.primaryOrder.addons[index].qty += 1;
}
console.log(orderStore.primaryOrder.addons);
},
decreaseAddonItemQty(item) {
let index = orderStore.primaryOrder.addons.findIndex(
(i) => i.id === item.id
);
if (index === -1) {
return;
} else {
// remove the item from the array if value goes 1->0
if (orderStore.primaryOrder.addons[index].qty === 1) {
console.log("removing item from array");
orderStore.primaryOrder.addons = _remove(
orderStore.primaryOrder.addons,
(i) => i.id !== item.id
);
console.log(orderStore.primaryOrder.addons);
return;
}
orderStore.primaryOrder.addons[index].qty -= 1;
}
}
})
The issue I'm running into has to do with the fact that one of my views consuming the orderStore.addons. My Product component is the consumer in this case:
const Product = (item) => {
const [qty, setQty] = useState(0);
const { id, label, thumbnailUrl, unitCost } = item;
autoEffect(() => {
if (orderStore.orderAddons.length === 0) {
setQty(0);
return;
}
console.log({ addons: orderStore.orderAddons });
let index = orderStore.orderAddons.findIndex((addon) => addon.id === id);
console.log({ index });
if (index !== -1) setQty(orderStore.findAddon(index).qty);
});
const Adder = () => {
return (
<div
className="flex"
style={{ flexDirection: "row", justifyContent: "space-between" }}
>
<div onClick={() => orderStore.decreaseAddonItemQty(item)}>-</div>
<div>{qty}</div>
<div onClick={() => orderStore.increaseAddonItemQty(item)}>+</div>
</div>
);
}
return (
<div>
<div>{label} {unitCost}</div>
<Adder />
</div>
)
}
export default view(Product)
The issue occurs when I call decreaseAddonItemQty and the item is removed from the addons array. The error is thrown in the Product component, stating that Uncaught TypeError: Cannot read property 'id' of undefined due to the fact that the array length reads as 2, despite the fact that the item has been removed ( see image below)
My assumption is that the consumer Product is reading the global store before it's completed updating, though of course I could be wrong.
What is the correct approach to use with react-easy-state to avoid this problem?
Seems like you found an auto batching bug. Just wrap your erroneous mutating code in batch until it is fixed to make it work correctly.
import { batch, store } from '#risingstack/react-easy-state'
const orderStore = store({
decreaseAddonItemQty(item) {
batch(() => {
// put your code here ...
})
}
})
Read the "Reactive renders are batched. Multiple synchronous store mutations won't result in multiple re-renders of the same component." section of the repo readme for more info about batching.
And some insight:
React updates are synchronous (as opposed to Angular and Vue) and Easy State (and all other state managers) use React setState behind the scenes to trigger re-renders. This means they are all synchronous too.
setState usually applies a big update at once while Easy State calls a dummy setState whenever you mutate a store property. This means Easy State would unnecessarily re-render way too often. To prevent this we have a batch method that blocks re-rendering until the whole contained code block is executed. This batch is automatically applied to most task sources so you don't have to worry about it, but if you call some mutating code from some exotic task source it won't be batched automatically.
We don't speak about batch a lot because it will (finally) become obsolete once Concurrent React is released. In the meantime, we are adding auto batching to as many places as possible. In the next update (in a few days) store methods will get auto batching, which will solve your issue.
You may wonder how could the absence of batching mess things up so badly. Older transparent reactivity systems (like MobX 4) would simply render the component a few times unnecessarily but they would work fine. This is because they use getters and setters to intercept get and set operations. Easy State (and MobX 5) however use Proxies which 'see a lot more'. In your case part of your browser's array.splice implementation is implemented in JS and Proxies intercept get/set operations inside array.splice. Probably array.splice is doing an array[2] = undefined before running array.length = 2 (this is just pseudo code of course). Without batching this results in exactly what you see.
I hope this helps and solves your issue until it is fixed (:
Edit: in the short term we plan to add a strict mode which will throw when store data is mutated outside store methods. This - combined with auto store method batching - will be the most complete solution to this issue until Concurrent React arrives.
Edit2: I would love to know why this was not properly batched by the auto-batch logic to cover this case with some tests. Is you repo public by any chance?
Let's say, I have a state that looks as follows.
constructor(props) {
super(props);
this.state = {
setting_a: "value-1",
setting_b: "color-green"
}
}
When I change the state of a specific setting (e.g. setting_a), I don't want other settings (e.g. setting_b) to disappear. So I also specify the other settings while changing the state. (which is easy using the spread operator ...state).
this.setState( {...this.state, setting_a: "value-2"});
I noticed though, that some tutorials restate them, and others only specify the changed key-values.
Things got just a little bit more complicated since the introduction of the Component#getDerivedStateFromProps method, ( since React 16.3 ).
static getDerivedStateFromProps(props, state) {
const oldSetting = state.setting_a;
const newSetting = props.setting_a;
if (oldSetting !== newSetting) {
// this is a very similar situation.
return ({ ...state, state.setting_a: props.setting_a});
}
return null;
}
Again, in the above example, I add all previous settings (i.e. ...state), because I don't want the other settings to be removed.
In both these cases, the same question: do I need to specifically repeat values which are already in the state ? Or are the states always merged incrementally, without removing ?
You don't need to copy the state (using spread operator or any idea) when updating the state with setState. The setState method updates the required state only:
this.setState( {setting_a: "value-2"});
So, now you will still get:
state = {
setting_a: "value-2",
setting_b: "color-green"
}
Similarly, it works like that when you return the object in getDerivedStateFromProps. The returned value is applied in the state without mutation.
You only need to copy the state when you want to update the property of state. For eg.:
// initial state
this.state = {
settings: {
a: 'value-1',
b: 'color-green'
}
}
Now, we have a and b property in settings state. So now, if you wanted to update the a, then you'll need to copy the settings:
this.setState((state) => ({settings: {...state.settings, a: 'value-2' } }))
The preceding example is with settings object. You can think similar with array of state. You may just do a google search for how to update the array without mutation?
It depends.
In your first case you could do:
this.setState( prevState => {
prevState.setting_a = "value-2";
return prevState
});
Or just go with:
this.setState({ setting_a: "value-2" });
As per React Docs State Updates are Merged.
The Set Up
I have a React/Redux application that loads a list of cats from an API.
The data gets loaded into a component like so:
// thunk, etc omitted for clarity.
componentDidMount() {
if(!this.props.loaded){
this.props.actions.loadRooms();
}
}
Which draws its props from here:
function mapStateToProps(state, ownProps) {
return {
cats: state.cats.items,
loaded: state.cats.loaded
}
}
Assume the following:
1) cats will be needed in a different, entirely separate component, one that is not a child of the current component.
2) I have no way of knowing which of the cats requiring components will be mounted first.
The Actual Question
Is the if(!this.props.loaded) useful? Put another way, does it save me a theoretical call to the API when that other route mounts if both check for existing store data first?
If the check is useful, is there a better way to do it?
Yes, I would have your redux actions look something like: GET_CATS, GET_CATS_SUCCESS, and GET_CATS_ERROR.
GET_CATS would set the loading state in the redux store to true, that way you can interrogate it in the respective componentDidMount() functions and only make the call to the api when loading is false. I think this is a fairly common way of doing it.
It all depends on how you handle your async data fetching in redux ,if both siblings components are listening to the portion of the state that represents cats you can do:
// Component A and Component B might have something like this
// they both subscribe to the same portion of the state so, if
// data is already available then you don't need to do fetch it again.
...
componentDidMount() {
if (this.props.cats.length === 0) {
this.props.actions.loadRooms();
}
}
...
If you are using redux-thunk then you might control this at the action level:
function loadRooms() {
return (dispatch, getState) => {
if (getState().cats.length === 0) {
dispatch(loadRoomsPending());
fetchMyData(...args)
.then((res) => dispatch(loadRoomsSuccess(res))
.catch((err) => dispatch(loadRoomsError(err));
}
}
}
// Component A and Component B
...
componentDidMount() {
this.props.actions.loadRooms();
}
...
Again here you have access to the current state with getState() so it's pretty common to check if the data is already available. Now this approach comes with some boilerplate and it might get tedious in the long run (it requires for you to write another three functions loadRoomsPending, loadRoomsSuccess, loadRoomsError). This way your components don't have to manually check for it. Or if you like it more explicit or cleaner you can give a middleware I implemented a try, I was kind of frustrated by all this boilerplate so using redux-slim-async you can do this:
function loadRooms() {
return {
types: [
actionTypes.LOAD_ROOMS_PENDING,
actionTypes.LOAD_ROOMS_SUCCESS,
actionTypes.LOAD_ROOMS_ERROR,
],
callAPI: fetch(...args).then(res => res.json()),
shouldCallAPI: (state) => state.cats.length === 0,
};
}
This handles everything for you with FSA compliant actions and it's very clear what is going on. Heck if you set it up properly you can make it even better:
function loadRooms() {
return {
typePrefix: actionTypes.LOAD_ROOMS,
callAPI: fetch(...args).then(res => res.json()),
shouldCallAPI: (state) => state.cats.length === 0,
};
}
And this will fire off the pending, success and error request with the format ${typePrefix}_PENDING, ${typePrefix}_SUCCESS, ${typePrefix}_ERROR, You can find the middleware here. But by all means just use whatever you feel best fits your use case, I felt like sharing this work because it's a frustration that brought me to build a middleware to handle it. Keep in mind that I made some assumptions on your case so if I am completely off let me know.
if I understand your question correctly, you want to be able to see if a separate class is loaded its data yet. If yes, then don't call the API to load the cats again.
There are two ways to do this, let's assumed COM1 and COM2 are your components.
return the entire state instead of just the specific variables you want for both of your components:
return state
then reference the cats in each component:
this.props.COM1.cats.items
this.props.COM2.cats.items
return the specific cats variable from the other components. you do the following for each components:
function mapStateToProps(state, ownProps) {
let cats = state.COM1.cats.items;
let loaded: state.cats.loaded;
let otherCats = state.COM2.cats.items;
return {
cats,
otherCats,
loaded
}
}
I am trying to generate an array of UI controls as they're being rendered and the resulting state contains duplicate entries. Is there a good way to protect against this?
This code is fired each time a component is rendered. There are multiple tables with similar controls and I'm using this to get the maximum number of rows per control:
const newcontrols: LoadedCtrls = {
"itemCtrlType": controlType,
"rowsCount": numberRows,
};
if (this.state.loadedcontrols.length == 0) {
this.setState({
loadedcontrols: [...this.state.loadedcontrols, newcontrols]
})
}
for (var i = 0; i < this.state.loadedcontrols.length; i++) {
if (this.state.loadedcontrols[i].itemCtrlType == controlType) {
if (this.state.loadedcontrols[i].rowsCount < numberRows) {
this.setState({
loadedcontrols: [...this.state.loadedcontrols.slice(0, i), newcontrols, ...this.state.loadedcontrols.slice(i)]
})
}
}
else {
this.setState({
loadedcontrols: [...this.state.loadedcontrols, newcontrols]
})
}
}
The result in the console is the following:
[{"itemCtrlType":"map1","rowsCount":2},{"itemCtrlType":"map2","rowsCount":3},{"itemCtrlType":"location","rowsCount":5},{"itemCtrlType":"map2","rowsCount":3},{"itemCtrlType":"monitor","rowsCount":7},{"itemCtrlType":"monitor","rowsCount":7}]
I've attempted to use good practices with immutability but it still seems like setState is firing off with duplicates, and I know it is an asynchronous operation. So is there a way to prevent this?
EDIT: The function is triggered each time a component's fetch function outputs data, and the state is set in the component:
.then(data => {
this.setState({
results: data,
loading: false,
}, () => {
this.finishLoad(this.state.controlType, this.state.customerId, data.length);
});
});
The use of setState() can lead to very uncertain behavior; for two reasons:
setState() should not be used as a synchronous call (i.e., you rely on state being updated immediately).
Calling setState() as you are iterating over the state may not have the intended behavior (because of 1.).
You might consider a more general pattern:
let localState=this.state;
// Rely on and modify localState, as desired
this.setState(localState) // Set final state and trigger potential re-render
You may not need to specify the callback in your initial setState() call since state changes are batched and the next render is implicitly invoked.
The following read may be helpful setState() State Mutation Operation May Be Synchronous In ReactJS