I'm trying to use a reselect selector as an argument to another selector:
import { createSelector } from 'reselect'
const productsSelector = state => {
return state.get('searchResults').get('products')
}
export const getSelectedProduct = createSelector(
productsSelector,
(products) => {
const selected = products.filter(product => product.shop.selected)
return selected[0]
}
)
export const getSelectedProductProduct = createSelector(
getSelectedProduct,
prod => {
const x = prod ? prod.products ? prod.products.find(produc => produc.selected) : getSelectedProduct : {}
if(prod) {
console.log(prod)
if ('prod', prod.products) {
console.log(prod.products)
if (prod.products.find(produ => produ.selected)) {
console.log('sel prod', prod.products.find(produ => produ.selected))
}
}
}
console.log(x)
}
)
getSelectedProduct is working and updating when it should update. However getSelectedProductProduct is not executing. What am I doing wrong?
They're used in a component:
const mapStateToProps = (state) => ({
region: state.get('map').get('region'),
markers: state.get('searchResults').get('products'),
selectedProduct: getSelectedProduct(state),
selectedProductProduct: getSelectedProductProduct(state)
})
action diff
redux state
The redux action which causes the diff in the top image does not cause getSelectedProductProduct to execute. I don't think the component knows that its field changed. I'm investigating this further. Wondering if the field is too deeply nested for the component to register the change.
It looks like getSelectedProduct filters searchResults.products.shop but the action is changing searchResults.products.products[1].selected. So getSelectedProduct returns the same value unchanged every time. That is why getSelectedProductProduct doesn't re-compute.
Related
I want to obtain a value from a variable hosted in redux and memorize it, to later compare it with itself and verify if it is different.
I need, for example, to do the following : const value = useSelector(state => state.data.value); (suppose that here the value is 0) now, when value changes, i need to compare it with the value it had previously
If you want to check what the value was on the previous render, you can save it in a ref:
const SomeComponent = () => {
const value = useSelector(state => state.data.value)
const prevRef = useRef();
useEffect(() => {
// Once the render is complete, update the ref's value
prevRef.current = value;
});
// Do something comparing prevRef.current and value
}
If you're doing this a lot you might find it useful to make a custom hook:
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.curren;t
}
// used like:
const SomeComponent = () => {
const value = useSelector(state => state.data.value)
const prev = usePrevious(value);
// Do something comparing prev and value.
}
You have to use selectors(i use 'reselect' library for that), such as:
file: selectors.js
import { createSelector } from 'reselect';
const stateEnvironments = state => state.environments;
export const selectEnvironments = createSelector([stateEnvironments],
environments => environments.data);
so then in your component you can use mapStateToProps with reselect and connect
import { createStructuredSelector } from 'reselect';
import { connect } from 'react-redux';
// your component here
const EnvironmentCurrencies = props => {
const {
data,
} = props;
return (
<div>
...
</div.
);
};
const mapStateToProps = createStructuredSelector({
data: selectEnvironments,
});
// here you can update your values with actions
// mapDispatchToProps is especially useful for constraining our actions to the connected component.
// You can access these via `this.props`.
const mapDispatchToProps = dispatch => ({
setEnvironment: environment => dispatch(actions.setEnvironment(environment)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Environments);
this is not a full working example and the version that you might use, can have a bit different synaxis, but I hope this gives you a kick start. If not, let me know, I'll add more extensive example
For learning purposes, I'm creating an e-shop, but I got stuck with localStorage, useEffect, and React context. Basically, I have a product catalog with a button for every item there that should add a product to the cart.
It also creates an object in localStorage with that item's id and amount, which you select when adding the product to the cart.
My context file:
import * as React from 'react';
const CartContext = React.createContext();
export const CartProvider = ({ children }) => {
const [cartProducts, setCartProducts] = React.useState([]);
const handleAddtoCart = React.useCallback((product) => {
setCartProducts([...cartProducts, product]);
localStorage.setItem('cartProductsObj', JSON.stringify([...cartProducts, product]));
}, [cartProducts]);
const cartContextValue = React.useMemo(() => ({
cartProducts,
addToCart: handleAddtoCart, // addToCart is added to the button which adds the product to the cart
}), [cartProducts, handleAddtoCart]);
return (
<CartContext.Provider value={cartContextValue}>{children}</CartContext.Provider>
);
};
export default CartContext;
When multiple products are added, then they're correctly displayed in localStorage. I tried to log the cartProducts in the console after adding multiple, but then only the most recent one is logged, even though there are multiple in localStorage.
My component where I'm facing the issue:
const CartProduct = () => {
const { cartProducts: cartProductsData } = React.useContext(CartContext);
const [cartProducts, setCartProducts] = React.useState([]);
React.useEffect(() => {
(async () => {
const productsObj = localStorage.getItem('cartProductsObj');
const retrievedProducts = JSON.parse(productsObj);
if (productsObj) {
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts([...cartProducts, fetchedProduct]);
});
}
}
)();
}, []);
console.log('cartProducts', cartProducts);
return (
<>
<pre>
{JSON.stringify(cartProductsData, null, 4)}
</pre>
</>
);
};
export default CartProduct;
My service file with fetchProductById function:
const domain = 'http://localhost:8000';
const databaseCollection = 'api/products';
const relationsParams = 'joinBy=categoryId&joinBy=typeId';
const fetchProductById = async (id) => {
const response = await fetch(`${domain}/${databaseCollection}/${id}?${relationsParams}`);
const product = await response.json();
return product;
};
const ProductService = {
fetchProductById,
};
export default ProductService;
As of now I just want to see all the products that I added to the cart in the console, but I can only see the most recent one. Can anyone see my mistake? Or maybe there's something that I missed?
This looks bad:
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts([...cartProducts, fetchedProduct]);
});
You run a loop, but cartProducts has the same value in every iteration
Either do this:
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts(cartProducts => [...cartProducts, fetchedProduct]);
});
Or this:
const values = Promise.all(Object.values(retrievedProducts).map(x => ProductService.fetchProductById(x.id)));
setCartProducts(values)
The last is better because it makes less state updates
Print the cartProducts inside useEffect to see if you see all the data
useEffect(() => {
console.log('cartProducts', cartProducts);
}, [cartProducts]);
if this line its returning corrects values
const productsObj = localStorage.getItem('cartProductsObj');
then the wrong will be in the if conditional: replace with
(async () => {
const productsObj = localStorage.getItem('cartProductsObj');
const retrievedProducts = JSON.parse(productsObj);
if (productsObj) {
Object.values(retrievedProducts).forEach(async (x) => {
const fetched = await ProductService.fetchProductById(x.id);
setCartProducts(cartProducts => [...fetched, fetchedProduct]);
});
}
}
Issue
When you call a state setter multiple times in a loop for example like in your case, React uses what's called Automatic Batching, and hence only the last call of a given state setter called multiple times apply.
Solution
In your useEffect in CartProduct component, call setCartProducts giving it a function updater, like so:
setCartProducts(prevCartProducts => [...prevCartProducts, fetchedProduct]);
The function updater gets always the recent state even though React has not re-rendered. React documentation says:
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.
I have a React component that maps state to props to get data via redux. Everything works fine with the action and the value being updated properly in the reducer. My only problem is that when the state value changes, I want my component to re render so that it is always displaying the most up to date value in the reducer. As of right now I have to call a separate function that refreshes the component, but I'd rather have it automatically re render every time that value changes in the reducer.
Action:
export const createPickup = (selected, pickups) => dispatch => {
let icon;
icon = check(selected);
pickups.icon = icon;
return API('/createPickUp/', {
...pickups,
})
.then(res => {
dispatch({type: types.CREATE_PICKUP, res});
})
.catch(err => console.log(err));
};
Reducer:
const initialState = {
pick: [],
};
export default function pickup(state = initialState, action) {
switch (action.type) {
case types.GET_PICK:
return {
pick: action.pickup,
};
case types.CREATE_PICKUP:
return {
pick: [action.res, ...state.pick],
};
case types.DEL_GAME:
return {
pick: state.pick.filter(p => p._id !== action.id),
};
case types.START_GAME:
return {
pick: state.pick.map(p =>
p._id === action.id ? {...p, start: true} : p,
),
};
case types.STOP_GAME:
return {
pick: state.pick.map(p =>
p._id === action.id ? {...p, stop: true} : p,
),
};
default:
return state;
}
}
Use useSelector hook in Functional Component as it automatically subscribes to the state and your component will re-render.
If you are using Class Component then use connect() from redux and mapStateinProps.
I am assuming you have passed the reducer to the global Store.
Now... make sure you have the up to date value in your component.. try consoling it like this...
import {useSelector} from 'react-redux';
const YourCmponent = () => {
const reduxState = useSelector(state => state);
console.log(reduxState);
return <div>Your Content</div>
}
That way you can get access to the redux store. And you don't need to make any other function for updating component You will always get updated value here.
I am implementing a context that manages all the messages of a conversation.
To reduce the complexity of my algorithm, I have decided to use a Map "sectionsRef" for accessing some stuff in O(1).
This map, needs to be updated inside my reducer's logic, where I update the stateful data, in order to synchronize both.
export function MessagesProvider({ children }) {
const [messages, dispatch] = useReducer(messagesReducer, initialState);
const sectionsRef = useMemo(() => new Map(), []);
const addMessages = (messages, unshift = false) => {
dispatch(actionCreators.addMessages(messages, unshift));
};
const addMessage = (message) => addMessages([message]);
const deleteMessage = (messageId) => {
dispatch(actionCreators.deleteMessage(messageId));
};
const value = useMemo(() => ({
messages,
addMessages,
deleteMessage,
// eslint-disable-next-line react-hooks/exhaustive-deps
}), [messages]);
return (
<MessagesContext.Provider value={value}>
{children}
</MessagesContext.Provider>
);
}
As you can see, I am using useMemo when initializing the Map in order to prevent re-initializations due to re-renders.
Is it correct to pass it as a payload to my reducer actions?
const addMessages = (messages, unshift = false) => {
dispatch(actionCreators.addMessages(messages, unshift, sectionsRef)); <---
};
To simplify my problem, imagine this is the real code:
//
// Reducer action
//
function reducerAction(state, messages, sectionsRef, title) {
state.push(...messages);
sectionsRef.set(title, state.length - 1);
}
//
// Context code
//
const state = [];
const firstMessagesSection = [{ id: 1 }];
const secondMessagesSection = [{ id: 1 }, { id: 2 }]
const sectionsRef = new Map();
reducerAction(state, firstMessagesSection, sectionsRef, "first section");
reducerAction(state, secondMessagesSection, sectionsRef, "second section");
console.log(state);
console.log(sectionsRef.get("second section"));
I am asking this because I have read that we shouldn't run side effects inside the reducers logic... so, if I need to synchronize that map with the state, what should I do instead?
Is it correct to pass it as a payload to my reducer actions?
No: reducers must be pure functions.
Redux describes reducers using a short list which I think is very useful:
Rules of Reducers​
We said earlier that reducers must always follow some special rules:
They should only calculate the new state value based on the state and action arguments
They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
They must not do any asynchronous logic or other "side effects"
The second and third items together describe pure functions, and the first one is just a Redux-specific convention.
In your example, you are violating two rules of pure functions:
mutating state with state.push(...messages) (rather than creating a new array and returning it), and
performing side-effects by modifying a variable in the outer scope: sectionsRef.set(title, state.length - 1)
Further, you seem to never use the Map (how is it accessed in your program?). It should be included in your context, and you can simply define it outside your component (its identity will never change so it won't cause a re-render).
Here's how you can refactor your code to achieve your goal:
Keep the reducer data pure:
// store.js
export function messagesReduer (messages, action) {
switch (action.type) {
case 'ADD': {
const {payload, unshift} = action;
return unshift ? [...payload, ...messages] : [...messages, ...payload];
}
case 'DELETE': {
const {payload} = action;
return messages.filter(m => m.id !== payload);
}
}
}
export const creators = {};
creators.add = (messages, unshift = false) => ({type: 'ADD', payload: messages, unshift});
creators.delete = (id) => ({type: 'DELETE', payload: id});
export const sections = new Map();
Update the Map at the same that you dispatch an action to the related state by combining those operations in a function:
// MessagesContext.jsx
import {
createContext,
useCallback,
useMemo,
useReducer,
} from 'react';
import {
creators,
messagesReduer,
sections,
} from './store';
export const MessagesContext = createContext();
export function MessagesProvider ({ children }) {
const [messages, dispatch] = useReducer(messagesReducer, []);
const addMessages = useCallback((title, messages, unshift = false) => {
dispatch(creators.add(messages, unshift));
sections.set(title, messages.length);
}, [creators.add, dispatch, messages]);
const addMessage = useCallback((title, message, unshift = false) => {
dispatch(creators.add([message], unshift));
sections.set(title, messages.length);
}, [creators.add, dispatch, messages]);
const deleteMessage = useCallback((id) => {
dispatch(creators.delete(id));
}, [creators.delete, dispatch]);
const value = useMemo(() => ({
addMessage,
addMessages,
deleteMessage,
messages,
sections,
}), [
addMessage,
addMessages,
deleteMessage,
messages,
sections,
]);
return (
<MessagesContext.Provider value={value}>
{children}
</MessagesContext.Provider>
);
}
Use the context:
// App.jsx
import {useContext} from 'react';
import {MessagesContext, MessagesProvider} from './MessagesContext';
function Messages () {
const {
// addMessage,
// addMessages,
// deleteMessage,
messages,
// sections,
} = useContext(MessagesContext);
return (
<ul>
{
messages.map(({id}, index) => (
<li key={id}>Message no. {index + 1}: ID {id}</li>
))
}
</ul>
);
}
export function App () {
return (
<MessagesProvider>
<Messages />
</MessagesProvider>
);
}
Additional notes:
Make sure your dependency lists (e.g. in useMemo, etc.) are exhaustive. Those lint warnings are there to help prevent you from making mistakes. In general, you should never need to suppress them.
I'm trying to display modal when no products have been selected by user before. I ended up having an infinite loop of useEffect() dependency. I'm not sure how to do it correctly in React.
import React, { useState, useEffect, useCallback } from 'react';
const MyComponent = ({ products }) => {
const [modals, setModals] = useState({});
const [currentModalName, setCurrentModalName] = useState('');
const setCurrentModal = useCallback(
(modalName, data = {}) => {
if (modalName) {
setModals({
...modals,
[modalName]: {
...modals[modalName],
...data
}
});
}
setCurrentModalName(modalName);
},
[modals]
);
useEffect(
() => {
if (!products.length) {
setCurrentModal('chooseProduct')
}
},
[products, setCurrentModal] // setCurrentModal causes infinite loop
);
return (
<div>...</div>
);
}
export default MyComponent;
I can just remove setCurrentModal from the dependencies, but I'm warned about it. If I add it, my React app freezes.
How can I organize my code to avoid freezing?
Why it loops?
The callback is always changing since it depends on the modals, which is always a different object even though it has the exact same properties as before, which always triggers the useEffect since it depends on the setCurrentModal callback value, which is always different since (() => {}) !== (() => {}).
Solution
Always use the functional update when the current state is needed to set the next state.
It'll prevent the need for the modals state as a dependency, which will limit the times when the callback is updated, fixing the infinite loop at the same time.
In addition to solving today's problem, functional update of the state is less prone to race-conditions, where multiple updates batched by React would overwrite each others.
const setCurrentModal = useCallback(
(modalName, data = {}) => {
if (!modalName) return; // precondition fail? early return.
// Happy path here!
// Note that I've used a different name to highlight that
// it's a different variable and to avoid shadowing the
// `modals` var from the outer scope.
setModals((currentModals) => ({ // use functional update.
...currentModals,
[modalName]: {
...currentModals[modalName],
...data
}
}));
setCurrentModalName(modalName);
},
// remove `modals` from the dependencies.
// setter functions are stable anyway, so it should remove any warning.
[setModals, setCurrentModalName]
);
useEffect(() => {
if (!products.length) {
setCurrentModal('chooseProduct')
}
},
[products, setCurrentModal]
);
Since the setCurrentModal callback is now stable (never ever changing), the useEffect will only be called when products value changes.
Missing dependencies warnings
The missing dependencies warnings come from the eslint-plugin-react-hooks, specifically, the react-hooks/exhaustive-deps rule. It's totally optional, but it helps keep the code clean and safe.
You could also choose to disable the warning just for this line:
const setCurrentModal = useCallback(
(modalName, data = {}) => {
// ...
setModals(/* ... */);
setCurrentModalName(modalName);
},
[] // eslint-disable-line react-hooks/exhaustive-deps
);
I think you can simplify it, without using useCallback.
(tested with Next.js and had no warnings, but if you still have some, you should use the answer of #Emile Bergeron)
import React, { useState, useEffect } from 'react'
const MyComponent = ({ products }) => {
const [modals, setModals] = useState({})
const [currentModalName, setCurrentModalName] = useState('')
const setCurrentModal = (name, data) => {
if (name) {
setModals(prev => {
return { ...prev, [name]: { ...prev[name], ...data }}
})
setCurrentModalName(name)
}
}
useEffect(() => {
if (!products || !products.length) {
const modalName = 'chooseProduct'
const data = { data: 'data' }
setCurrentModal(modalName, data)
}
}, [products])
const modalsJsx = modals ? Object.keys(modals).map((x, i) => {
return <li key={`modal-${i}`}>{x}</li>
}) : ''
const addModal = () => {
const name = 'test' + Math.floor(Math.random() * Math.floor(300))
setCurrentModal(name, { data: 'Hey' })
}
return (
<div>
<p>Current Modal : {currentModalName}</p>
<p>Modals : </p>
<ul>
{modalsJsx}
</ul>
<button onClick={addModal}>Test</button>
</div>
)
}
export default MyComponent
The function with useCallback to avoid warnings :
const setCurrentModal = useCallback((name, data = {}) => {
if (name) {
setModals(prev => {
return { ...prev, [name]: { ...prev[name], ...data }}
})
setCurrentModalName(name)
}
}, [setModals, setCurrentModalName])