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.
Related
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.
I've been following along the REDUX essentials guide and I'm at part 8, combining RTK Query with the createEntityAdapter. I'm using the guide to implement it in a personal project where my getUni endpoint has an argument named country, as you can see from the code snippet below.
I'm wondering is there anyway to access the country argument value from the state in universityAdaptor.getSelector(state => ) at the bottom of the snippet, as the query key name keeps changing.
import {
createEntityAdapter,
createSelector,
nanoid
} from "#reduxjs/toolkit";
import {
apiSlice
} from "../api/apiSlice";
const universityAdapter = createEntityAdapter({})
const initialState = universityAdapter.getInitialState();
export const extendedApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getUni: builder.query({
query: country => ({
url: `http://universities.hipolabs.com/search?country=${country}`,
}),
transformResponse: responseData => {
let resConvert = responseData.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map(each => {
return { ...each,
id: nanoid()
}
});
return universityAdapter.setAll(initialState, resConvert)
}
})
})
});
export const {
useGetUniQuery
} = extendedApiSlice;
export const {
selectAll: getAllUniversity
} = universityAdapter.getSelectors(state => {
return Object.keys({ ...state.api.queries[<DYNAMIC_QUERY_NAME>]data }).length === 0
? initialState : { ...state.api.queries[<DYNAMIC_QUERY_NAME>]data }
})
UPDATE: I got it working with a turnery operator due to the multiple redux Actions created when RTK Query handles fetching. Wondering if this is best practice as I still haven't figured out how to access the country argument.
export const { selectAll: getAllUniversity } = universityAdapter
.getSelectors(state => {
return !Object.values(state.api.queries)[0]
? initialState : Object.values(state.api.queries)[0].status !== 'fulfilled'
? initialState : Object.values(state.api.queries)[0].data
})
I wrote that "Essentials" tutorial :)
I'm actually a bit confused what your question is - can you clarify what specifically you're trying to do?
That said, I'll try to offer some hopefully relevant info.
First, you don't need to manually call someEndpoint.select() most of the time - instead, call const { data } = useGetThingQuery("someArg"), and RTKQ will fetch and return it. You only need to call someEndpoint.select() if you're manually constructing a selector for use elsewhere.
Second, if you are manually trying to construct a selector, keep in mind that the point of someEndpoint.select() is to construct "a selector that gives you back the entire cache entry for that cache key". What you usually want from that cache entry is just the received value, which is stored as cacheEntry.data, and in this case that will contain the normalized { ids : [], entities: {} } lookup table you returned from transformResponse().
Notionally, you might be able to do something like this:
const selectNormalizedPokemonData = someApi.endpoints.getAllPokemon.select();
// These selectors expect the entity state as an arg,
// not the entire Redux root state:
// https://redux-toolkit.js.org/api/createEntityAdapter#selector-functions
const localizedPokemonSelectors = pokemonAdapter.getSelectors();
const selectPokemonEntryById = createSelector(
selectNormalizedPokemonData ,
(state, pokemonId) => pokemonId,
(pokemonData, pokemonId) => {
return localizedPokemonSelectors.selectById(pokemonData, pokemonId);
}
)
Some more info that may help see what's happening with the code in the Essentials tutorial, background - getLists endpoint takes 1 parameter, select in the service:
export const getListsResult = (state: RootState) => {
return state.tribeId ? extendedApi.endpoints.getLists.select(state.tribeId) : [];
};
And my selector in the slice:
export const selectAllLists = createSelector(getListsResult, (listsResult) => {
console.log('inside of selectAllLists selector = ', listsResult);
return listsResult.data;
// return useSelector(listsResult) ?? [];
});
Now this console logs listsResult as Æ’ memoized() { function! Not something that can have .data property as tutorial suggests. Additionally return useSelector(listsResult) - makes it work, by executing the memoized function.
This is how far I got, but from what I understand, the code in the Essentials tutorial does not work as it is...
However going here https://codesandbox.io/s/distracted-chandrasekhar-r4mcn1?file=/src/features/users/usersSlice.js and adding same console log:
const selectUsersData = createSelector(selectUsersResult, (usersResult) => {
console.log("usersResult", usersResult);
return usersResult.data;
});
Shows it is not returning a memorised function, but an object with data on it instead.
Wonder if the difference happening because I have a parameter on my endpoint...
select returns a memoized curry function. Thus, call it with first with corresponding arg aka tribeId in your case and then with state. This will give you the result object back for corresponding chained selectors.
export const getListsResult = (state: RootState) => {
return state.tribeId ? extendedApi.endpoints.getLists.select(state.tribeId)(state) : [];
};
The intention of the getUni endpoint was to produce an array of university data. To implement the .getSelector function to retrieve that array, I looped over all query values, searching for a getUni query and ensuring they were fulfilled. The bottom turnery operator confirms the getUni endpoint was fired at least once otherwise, it returns the initialState value.
export const { selectAll: getAllUniversity } = universityAdapter
.getSelectors(state => {
let newObj = {};
for (const value of Object.values(state.api.queries)) {
if (value?.endpointName === 'getUni' && value?.status === 'fulfilled') {
newObj = value.data;
}
}
return !Object.values(newObj)[0] ? initialState : newObj;
})
Recently I contemplated the idea of having central state management in my React apps without using Redux or Mobx, instead opting to create something similar to the application class in Android. In any event, I implemented something similar to this:
Create a store folder and a file called store.js in it whose contents are:
// State
let state = {
users: {},
value: 0
};
// Stores references to component functions
let triggers = [];
// Subscription Methods
export const subscribe = trigger => {
triggers.push(trigger);
trigger();
}
export const unsubscribe = trigger => {
let pos = -1;
for (let i in triggers) {
if (triggers[i]===trigger) {
pos = i;
break;
}
}
if (pos!==-1) {
triggers.splice(pos, 1);
}
}
// Trigger Methods
let triggerAll = () => {
for (let trigger of triggers) {
trigger();
}
}
// State Interaction Methods
export const setUser = (name, description) => {
state.users[name] = description;
triggerAll();
}
export const removeUser = name => {
if (name in state.users) {
delete state.users[name];
}
triggerAll();
}
export const getAllUsers = () => {
return state.users;
}
export const getUser = name => {
if (!(name in state.users)) {
return null;
}
return state.users[name];
}
export const getValue = () => {
return state.value;
}
export const setValue = value => {
state.value = value;
triggerAll();
}
And connecting to this store in the following manner:
// External Modules
import React, { Component } from 'react';
import {Box, Text, Heading} from 'grommet';
// Store
import {subscribe, unsubscribe, getAllUsers} from '../../store/store';
class Users extends Component {
state = {
users: []
}
componentDidMount() {
subscribe(this.trigger); // push the trigger when the component mounts
}
componentWillUnmount() {
unsubscribe(this.trigger); // remove the trigger when the component is about to unmount
}
// function that gets triggered whenever state in store.js changes
trigger = () => {
let Users = getAllUsers();
let users = [];
for (let user in Users) {
users.push({
name: user,
description: Users[user]
});
}
this.setState({users});
}
render() {
return <Box align="center">
{this.state.users.map(user => {
return <Box
style={{cursor: "pointer"}}
width="500px"
background={{color: "#EBE7F3"}}
key={user.name}
round
pad="medium"
margin="medium"
onClick={() => this.props.history.push("/users/" + user.name)}>
<Heading margin={{top: "xsmall", left: "xsmall", right: "xsmall", bottom: "xsmall"}}>{user.name}</Heading>
<Text>{user.description}</Text>
</Box>
})}
</Box>;
}
}
export default Users;
Note. I've tested this pattern on a website and it works. Check it out here. And I apologize I am trying to keep the question concise for stackoverflow, I've provided a more detailed explanation of the pattern's implementation here
But anyway, my main question, what could be the possible reasons not to use this, since I assume if it was this simple, people wouldn't be using Redux or Mobx. Thanks in advance.
That's what Redux and MobX basically do, you are wrong in thinking that at their core concept they are much different. Their size and complexity came as a result of their effort to neutralize bugs and adapt to a vast variety of application cases. That's it. Although they might be approaching the task from different angles, but the central concept is just that. Maybe you should familiarize your self with what they actually do underneath.
Btw, you do not need to store redundant state in your component, if all you need is to trigger the update. You can just call forceUpdate() directly:
// function that gets triggered whenever state in store.js changes
trigger = () => {
this.forceUpdate();
}
That's similar to what Redux and MobX bindings for react do under the hood.
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.