Issue
I am having a rerendering issue that is occurring. For some reason the state I update in the store object does not propogate to the components. It doesn't have the most up to date state.
Notice how the object before the 🔥 had 63: null. I deleted the object there in the store, but in the state when I call syncPhotoshopLayersAndContexts from components. However, the state the components get back isn't updated.
Stack:
immer
zustand
TypeScript
List item
export const useContextStore = create(
immer((set: any, get: any) => ({
// this data structure is the main context store. This is what we update the app with.
layerID2Context: {},
// this data structure is the cache we used to restore from, the user will hit ctrl+z at some point.
contextCache: {},
setAILayerContext: (layerID: number, layerContext: LayerAIContext) =>
set((state: ContextStoreState) => {
state.layerID2Context[layerID] = layerContext;
}),
getAILayerContext: (layerID: number) => get().layerID2Context[layerID],
removeAILayerContext: (layerID: number) => {
let layerContext = get().getAILayerContext(layerID)
console.log("below is the context being removed")
console.log(layerContext)
set((state: ContextStoreState) => {
// making a copy for good measure
let newContext = {
...layerContext
}
// storing previous version in cache for a back up in case of an undo.
state.contextCache[layerID] = newContext
delete state.layerID2Context[layerID]
})
},
retreiveContextFromCache: (layerID: number) => {
return get().contextCache[layerID]
},
syncPhotoshopLayersAndContexts: (layers: Array<Layer>) => {
let activeLayerIDsWithContexts = Object.keys(get().layerID2Context).map(key => parseInt(key))
set((state: ContextStoreState) => {
let activeLayerIDsInApp = layers.map(layer => layer.id)
let layerIDsToCache = activeLayerIDsWithContexts.filter(x => !activeLayerIDsInApp.includes(x));
console.log(layerIDsToCache)
console.log("🔥🔥🔥🔥🔥🔥🔥")
console.log(get().layerID2Context)
for(let layerID of layerIDsToCache){
// state.removeAILayerContext(layerID)
state.setAILayerContext(layerID, null)
}
console.log(get().layerID2Context)
console.log("🔥🔥🔥🔥🔥🔥🔥")
})
}
}))
);
Here is the where the log is being displayed in the frontend component. However, it has two contexts, when it should have just the one that isn't null.
Ask
What could the issue be? Why is my component not getting the updated state? It does exist at some point, just never given to the component to be rendered out.
Related
I'm right on the verge of tossing React and just using vanilla JS but thought I'd check here first. I'm simply trying to pass the contents of a variable, which contains an object, into state and have that update the element that depends upon it. If I pass setState a variable containing the object, it doesn't work. If I pass it the explicit text of the object it does.
Using React v 18.0.0
function buttonHandler(e) {
e.preventDefault()
let tmpObject = {...chartData}
tmpObject.datasets[0].data = quoteData.map(entry => entry['3'])
tmpObject.datasets[1].data = quoteData.map(({fastEma}) => fastEma)
tmpObject.datasets[2].data = quoteData.map(({slowEma}) => slowEma)
tmpObject.labels = quoteData.map(entry => new Date(entry.timestamp).toLocaleTimeString())
console.log("from button:", tmpObject)
setChartData(prevState => {
console.log("tmpObject",tmpObject)
return tmpObject
})
return <div>
<button onClick={buttonHandler}>Update</button>
<Line options={chartOptions} data={chartData}/>
</div>
When I run the above, the output of the console.log is exactly as it should be but the element does not update. If I copy the object output from the console and paste it explicitly into the code it does work.
function buttonHandler(e) {
e.preventDefault()
setChartData({...})
I've tried every imaginable variation on the below statement to no avail...
return {...prevState, ...tmpObject}
I'd greatly appreciate any suggestions.
EDIT:
As another test, I added the following HTML element to see if it got updated. It gets updated and shows the expected data. Still, I'm having a hard time understanding why the chart will update if I pass it explicit text but will not if I pass it a variable.
<p>{`${new Date().toLocaleTimeString()} {JSON.stringify(chartData)}`</p>
The issue is that of state mutation. Even though you've shallow copied the chartData state you should keep in mind that this is a copy by reference. Each property is still a reference back into the original chartData object.
function buttonHandler(e) {
e.preventDefault();
let tmpObject = { ...chartData }; // <-- shallow copy ok
tmpObject.datasets[0].data = quoteData.map(entry => entry['3']); // <-- mutation!!
tmpObject.datasets[1].data = quoteData.map(({ fastEma }) => fastEma); // <-- mutation!!
tmpObject.datasets[2].data = quoteData.map(({ slowEma }) => slowEma); // <-- mutation!!
tmpObject.labels = quoteData.map(
entry => new Date(entry.timestamp).toLocaleTimeString()
);
console.log("from button:", tmpObject);
setChartData(prevState => {
console.log("tmpObject",tmpObject);
return tmpObject;
});
}
In React not only does the next state need to be a new object reference, but so does any nested state that is being update.
See Immutable Update Pattern - It's a Redux doc but really explains why using mutable updates is key in React.
function buttonHandler(e) {
e.preventDefault();
setChartData(chartData => {
const newChartData = {
...chartData, // <-- shallow copy previous state
labels: quoteData.map(
entry => new Date(entry.timestamp).toLocaleTimeString()
),
datasets: chartData.datasets.slice(), // <-- new datasets array
};
newChartData.datasets[0] = {
...newChartData.datasets[0], // <-- shallow copy
data: quoteData.map(entry => entry['3']), // <-- then update
};
newChartData.datasets[1] = {
...newChartData.datasets[1], // <-- shallow copy
data: quoteData.map(({ fastEma }) => fastEma), // <-- then update
};
newChartData.datasets[2] = {
newChartData.datasets[2], // <-- shallow copy
data: quoteData.map(({ slowEma }) => slowEma), // <-- then update
};
return newChartData;
});
}
Check your work with an useEffect hook with a dependency on the chartData state:
useEffect(() => {
console.log({ chartData });
}, [chartData]);
If there's still updating issue then check the code of the Line component to see if it's doing any sort of mounting memoization of the passed data prop.
I'm using custom hooks for a component, and the custom hook uses a custom context. Consider
/* assume FooContext has { state: FooState, dispatch: () => any } */
const useFoo = () => {
const { state, dispatch } = useContext(FooContextContext)
return {apiCallable : () => apiCall(state) }
}
const Foo = () => {
const { apiCallable } = useFoo()
return (
<Button onClick={apiCallable}/>
)
}
Lots of components will be making changes to FooState from other components (form inputs, etc.). It looks to me like Foo uses useFoo, which uses state from FooStateContext. Does this mean every change to FooContext will re-render the Foo component? It only needs to make use of state when someone clicks the button but never otherwise. Seems wasteful.
I was thinking useCallback is specifically for this, so I am thinking return {apiCallable : useCallback(() => apiCall(state)) } but then I need to add [state] as a second param of useCallback. Then that means the callback will be re-rendered whenever state updates, so I'm back at the same issue, right?
This is my first time doing custom hooks like this. Having real difficulty understanding useCallback. How do I accomplish what I want?
Edit Put another way, I have lots of components that will dispatch small changes to deeply nested properties of this state, but this particular component must send the entire state object via a RESTful API, but otherwise will never use the state. It's irrelevant for rendering this component completely. I want to make it so this component never renders even when I'm making changes constantly to the state via keypresses on inputs (for example).
Since you provided Typescript types in your question, I will use them in my response.
Way One: Split Your Context
Given a context of the following type:
type ItemContext = {
items: Item[];
addItem: (item: Item) => void;
removeItem: (index: number) => void;
}
You could split the context into two separate contexts with the following types:
type ItemContext = Item[];
type ItemActionContext = {
addItem: (item: Item) => void;
removeItem: (index: number) => void;
}
The providing component would then handle the interaction between these two contexts:
const ItemContextProvider = () => {
const [items, setItems] = useState([]);
const actions = useMemo(() => {
return {
addItem: (item: Item) => {
setItems(currentItems => [...currentItems, item]);
},
removeItem: (index: number) => {
setItems(currentItems => currentItems.filter((item, i) => index === i));
}
};
}, [setItems]);
return (
<ItemActionContext.Provider value={actions}>
<ItemContext.Provider value={items}>
{children}
</ItemContext.Provider>
</ItemActionContext.Provider>
)
};
This would allow you to get access to two different contexts that are part of one larger combined context.
The base ItemContext would update as items are added and removed causing rerenders for anything that was consuming it.
The assoicated ItemActionContext would never update (setState functions do not change for their lifetime) and would never directly cause a rerender for a consuming component.
Way Two: Some Version of an Subscription Based Value
If you make the value of your context never change (mutate instead of replace, HAS THE WORLD GONE CRAZY?!) you can set up a simple object that holds the data you need access to and minimises rerenders, kind of like a poor mans Redux (maybe it's just time to use Redux?).
If you make a class similar to the following:
type Subscription<T> = (val: T) => void;
type Unsubscribe = () => void;
class SubscribableValue<T> {
private subscriptions: Subscription<T>[] = [];
private value: T;
constructor(val: T) {
this.value = val;
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.subscribe = this.subscribe.bind(this);
}
public get(): T {
return this._val;
}
public set(val: T) {
if (this.value !== val) {
this.value = val;
this.subscriptions.forEach(s => {
s(val)
});
}
}
public subscribe(subscription: Subscription<T>): Unsubscriber {
this.subscriptions.push(subscription);
return () => {
this.subscriptions = this.subscriptions.filter(s => s !== subscription);
};
}
}
A context of the following type could then be created:
type ItemContext = SubscribableValue<Item[]>;
The providing component would look something similar to:
const ItemContextProvider = () => {
const subscribableValue = useMemo(() => new SubscribableValue<Item[]>([]), []);
return (
<ItemContext.Provider value={subscribableValue}>
{children}
</ItemContext.Provider>
)
};
You could then use some a custom hooks to access the value as needed:
// Get access to actions to add or remove an item.
const useItemContextActions = () => {
const subscribableValue = useContext(ItemContext);
const addItem = (item: Item) => subscribableValue.set([...subscribableValue.get(), item]);
const removeItem = (index: number) => subscribableValue.set(subscribableValue.get().filter((item, i) => i === index));
return {
addItem,
removeItem
}
}
type Selector = (items: Item[]) => any;
// get access to data stored in the subscribable value.
// can provide a selector which will check if the value has change each "set"
// action before updating the state.
const useItemContextValue = (selector: Selector) => {
const subscribableValue = useContext(ItemContext);
const selectorRef = useRef(selector ?? (items: Item[]) => items)
const [value, setValue] = useState(selectorRef.current(subscribableValue.get()));
const useEffect(() => {
const unsubscribe = subscribableValue.subscribe(items => {
const newValue = selectorRef.current(items);
if (newValue !== value) {
setValue(newValue);
}
})
return () => {
unsubscribe();
};
}, [value, selectorRef, setValue]);
return value;
}
This would allow you to reduce rerenders using selector functions (like an extremely basic version of React Redux's useSelector) as the subscribable value (root object) would never change reference for its lifetime.
The downside of this is that you have to manage the subscriptions and always use the set function to update the held value to ensure that the subscriptions will be notified.
Conclusion:
There are probably a number of other ways that different people would attack this problem and you will have to find one that suits your exact issue.
There are third party libraries (like Redux) that could also help you with this if your context / state requirements have a larger scope.
Does this mean every change to FooContext will re-render the Foo component?
Currently (v17), there is no bailout for Context API. Check my another answer for examples. So yes, it will always rerender on context change.
It only needs to make use of state when someone clicks the button but never otherwise. Seems wasteful.
Can be fixed by splitting context providers, see the same answer above for explanation.
I am implementing Reselect in my project and have a little confusion on how to properly use it. After following multiple tutorials and articles about how to use reselect, I have used same patterns and still somethings dont work as expected.
My selector:
const getBaseInfo = (state) => state.Info;
const getResources = (state) => state.Resources;
export const ASelector = createSelector(
[getBaseInfo, getResources],
(items, resources) => {
let result = {};
for(const item in items) {
console.log(item);
result[item] = _.pick(items[item], ['Title', 'Type', 'Beginning', 'minAmount', 'Address'])
}
for(const item in resources) {
console.log(item);
result[item] = {...result[item], firstImage: resources[item].firstImage}
}
return result;
}
);
mapStateToProps component:
function mapStateToProps(state) {
console.log(state);
return {
gridInfo: ASelector(state)
}
}
Now at first my initial state is:
state = { Info: {}, Resources: {} }
My Reducer:
const Info = ArrayToDictionary.Info(action.payload.data.Info);
const Resources = ArrayToDictionary.Resources(action.payload.data.Info);
let resourcesKeys = Object.keys(Resources);
let infoKeys = Object.keys(Info);
let temp = { ...state };
let newInfo;
for (let item of infoKeys) {
newInfo = {
Title: Info[item].Title,
Type: Info[item].Type,
BeginningOfInvesting: Info[item].BeginningOfInvesting,
DateOfEstablishment: Info[item].DateOfEstablishment,
pricePerUnit: Info[item].PricePerUnit,
minUnits: Info[item].MinUnits,
publicAmount: Info[item].PublicAmount,
minInvestmentAmount: Info[item].MinInvestmentAmount,
EinNumber: Info[item].EinNumber,
Address: Info[item].Address,
Status: Info[item].Status,
Lat: Info[item].Lat,
Lng: Info[item].Lng,
CurrencySymbol: Info[item].CurrencySymbol,
Publicity: Info[item].Publicity
}
temp.Info[item] = { ...temp.Info[item], ...newInfo }
}
for (let item of resourcesKeys) {
temp.Resources[item] = { ...temp.Resources[item], ...Resources[item] }
}
return temp;
As a component renders with the initial state, I have an action pulling data from api and saving it accordingly into the state inside reducers.
Now my state is changed, but after debugging a little into reselects code, I found in the comparison function that the old and new states are the same.
Suddenly my "old" state became already populated with the newState data and it of course failing the comparison as they became the same.
Is there anything wrong with my selectors?
I have really tried to use it as the documentation states, but still cant understand how to solve my little issue.
Thank you very much for reading and helping!
It looks like the temp.Info[item] and temp.Resources[item] lines are mutating the existing state. You've made a shallow copy of the top level, but aren't correctly copying the second level. See the Immutable Update Patterns page in the Redux docs for an explanation of why this is an issue and what to do instead.
You might want to try using the immer library to simplify your immutable update logic. Also, our new redux-starter-kit library uses Immer internally.
In my database I have more than 1,000,000 stored items. When I want showing them in my React App, I use a simple Component that fetch first 100 items from database, store them in Redux Store and display in the list (it uses react-virtualized to keep a few items in the DOM). When user scrolls down and the item 50 (for example) is visualized, the Component fetchs next 100 items from database and store them in Redux store again. At this point, I have 200 items stored in my Redux store. When the scroll down process continues, it gets to the point where the Redux store keeps a lot of items:
ItemsActions.js
const fetchItems = (start=0, limit=100) => dispatch => {
dispatch(fetchItemsBegin())
api.get(`items?_start=${start}&_limit=${limit}`).then(
res => {
dispatch(fetchItemsSuccess(res.data))
},
error => {
dispatch(fetchItemsError(error))
}
)
}
const fetchItemsBegin = () => {
return {
type: FETCH_ITEMS_BEGIN
}
}
const fetchItemsSuccess = items => {
return {
type: FETCH_ITEMS_SUCCESS,
payload: {
items
}
}
}
MyComponent.jsx
state = {
currentIndex: 0,
currentLimit: 100
}
const mapStateToProps = ({ items }) => ({
data: Object.values(items.byId)
})
const mapDispatchToProps = dispatch => ({
fetchItems: (...args) => dispatch(fetchItems(...args)),
})
componentDidMount() {
this.props.fetchItems(0,100)
}
onScroll(index) {
...
if (currentIndex > currentLimit - 50) {
this.props.fetchItem([newIndex],100)
}
}
render() {
return (
this.props.data.map(...)
)
}
First Question:
Is necessary to store all items in Redux?? If current index of the list displayed is 300,000, should I have 300,000 items in Redux Store? or should I have stored the 200 displayed items? How I have to handle the FETCH_ITEMS_SUCCESS action in the reducer file?
ItemsReducer.js
case FETCH_ITEMS_SUCCESS:
return {
...state, ????
...action.payload.items.reduce((acc, item) => ({
...acc,
[item.id]: item
}), {})
}
Second Question:
In the same Component, I have also a filter section to display items which meet the indicated conditions. I have only a few items active in the DOM and, depending on first question, others items in Redux Store, but filters must be applied to 1,000,000 items stored in the database. How I handle this situation?
Conclusion
I don't know how I should handle a large amount of items stored in the backend. How many items should have the Redux store in each situation?
Thanks in advance.
I have a React Redux app which gets data from my server and displays that data.
I am displaying the data in my parent container with something like:
render(){
var dataList = this.props.data.map( (data)=> <CustomComponent key={data.id}> data.name </CustomComponent>)
return (
<div>
{dataList}
</div>
)
}
When I interact with my app, sometimes, I need to update a specific CustomComponent.
Since each CustomComponent has an id I send that to my server with some data about what the user chose. (ie it's a form)
The server responds with the updated object for that id.
And in my redux module, I iterate through my current data state and find the object whose id's
export function receiveNewData(id){
return (dispatch, getState) => {
const currentData = getState().data
for (var i=0; i < currentData.length; i++){
if (currentData[i] === id) {
const updatedDataObject = Object.assign({},currentData[i], {newParam:"blahBlah"})
allUpdatedData = [
...currentData.slice(0,i),
updatedDataObject,
...currentData.slice(i+1)
]
dispatch(updateData(allUpdatedData))
break
}
}
}
}
const updateData = createAction("UPDATE_DATA")
createAction comes from redux-actions which basically creates an object of {type, payload}. (It standardizes action creators)
Anyways, from this example you can see that each time I have a change I constantly iterate through my entire array to identify which object is changing.
This seems inefficient to me considering I already have the id of that object.
I'm wondering if there is a better way to handle this for React / Redux? Any suggestions?
Your action creator is doing too much. It's taking on work that belongs in the reducer. All your action creator need do is announce what to change, not how to change it. e.g.
export function updateData(id, data) {
return {
type: 'UPDATE_DATA',
id: id,
data: data
};
}
Now move all that logic into the reducer. e.g.
case 'UPDATE_DATA':
const index = state.items.findIndex((item) => item.id === action.id);
return Object.assign({}, state, {
items: [
...state.items.slice(0, index),
Object.assign({}, state.items[index], action.data),
...state.items.slice(index + 1)
]
});
If you're worried about the O(n) call of Array#findIndex, then consider re-indexing your data with normalizr (or something similar). However only do this if you're experiencing performance problems; it shouldn't be necessary with small data sets.
Why not using an object indexed by id? You'll then only have to access the property of your object using it.
const data = { 1: { id: 1, name: 'one' }, 2: { id: 2, name: 'two' } }
Then your render will look like this:
render () {
return (
<div>
{Object.keys(this.props.data).forEach(key => {
const data = this.props.data[key]
return <CustomComponent key={data.id}>{data.name}</CustomComponent>
})}
</div>
)
}
And your receive data action, I updated a bit:
export function receiveNewData (id) {
return (dispatch, getState) => {
const currentData = getState().data
dispatch(updateData({
...currentData,
[id]: {
...currentData[id],
{ newParam: 'blahBlah' }
}
}))
}
}
Though I agree with David that a lot of the action logic should be moved to your reducer handler.