I have an app like this
function App(){
const [appState, setAppState] = useState(
{
questions: []
}
)
const addToQuestion = (questionObject) =>{
setAppState((prevState) => {
return {...prevState, questions: [...prevState.questions, questionObject]}
})
}
let removeFromQuestionArray = () => {
setAppState((prevState) => {
let a = prevState.questions
a.pop()
return {...prevState, questions: a}
})
}
const onBackButtonClicked = () => {
removeFromQuestionArray()
}
}
But when I call removeFromQuestionsArray() the setAppState works unpredictably. It sometimes removes all the items in the array even though I'm only popping the last item in the array. What could be causing this. I just want to remove the last item from the array, how can I go about this without having issues?
Your code works fine.
Posting this as an answer to show the snippet:
Note: I edited the addToQuestions function to make it work for the example, but I didn't touch removeFromQuestionsArray
const { useState, useRef } = React
function App(){
const [appState, setAppState] = useState(
{
questions: [
'What is Love',
'Whats the meaning of life',
'Whats the answer to this question'
]
}
)
const input = useRef();
const addToQuestion = () =>{
let question = input.current.value;
setAppState((prevState) => {
return {...prevState, questions: [...prevState.questions, question]}
})
input.current.value = '';
input.current.focus();
}
let removeFromQuestionArray = () => {
setAppState((prevState) => {
let a = prevState.questions
a.pop()
return {...prevState, questions: a}
})
}
return (
<div>
{appState.questions.map(question => <li>{question}</li>)}
<input ref={input}></input>
<button onClick={addToQuestion}>Add Question</button>
<button onClick={removeFromQuestionArray}>Remove</button>
</div>
)
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>
,
root
)
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Related
I want to add items to an array with the useState hook instead of doing array.push. This is the original code:
let tags = []
data.blog.posts.map(post => {
post.frontmatter.tags.forEach(tag => {
if (!tags.includes(tag)){
tags.push(tag)
}
})
})
This is one of several things I've tried with React:
const [tags, setTags] = useState([])
data.blog.posts.map(post => {
post.frontmatter.tags.map(tag => {
if (!tags.includes(tag)){
setTags(tags => [...tags, tag])
}
})
})
The "tags" state variable does not receive anything in the above example.
I have looked at a variety of similar threads but the problems and solutions there are difficult to translate to this situation.
You can try setting the tags state in initial render or on any event as per your requirement .
const [tags, setTags] = useState([]);
useEffect(()=>{
const arr=[];
data.blog.posts.map(post => {
post.frontmatter.tags.map(tag => {
if (!arr.includes(tag)){
arr.push(tag)
}
})
});
setTags([...arr]);
},[]);
Ok, I did understand what you wanted to do.
Here is the code and I did add some commest and there is also a working code sandbox
so it will show the "tags" you have on your state and when you click on the button it will filter and add those tags that are missing
import React, { useState } from "react";
//mock data.blog.posts
const data = {
blog: {
posts: [
{
frontmatter: {
tags: ["tag1", "tag2", "tag3"]
}
}
]
}
};
const App = () => {
const [tags, setTags] = useState(["tag1"]);
const filterTags = () => {
const myTags = ["tag1"];
let result;
data.blog.posts.map((post) => {
// check what tags are not included in tag stateon line 18
result = post.frontmatter.tags.filter((item) => !tags.includes(item));
});
// here it will show that 'tag2' and 'tag3' does not exist
console.log("result", result);
// here we are setting the state
setTags((oldState) => [...oldState, ...result]);
};
return (
<div className="App">
<h1>My tags</h1>
{tags.map((tag) => (
<h4>{tag}</h4>
))}
<button onClick={() => filterTags()}>add tags</button>
<hr />
<h1>My tags from posts</h1>
{data.blog.posts.map((posts) => {
return posts.frontmatter.tags.map((tag) => <div>{tag}</div>);
})}
</div>
);
};
export default App;
and here is the codeSandBox
In my Table component I'm getting data from redux store with useSelector hook.
const info = useSelector(state => {
if (type === 'catalog') {
return store.getState().catalog.products
}
if (type === 'category') {
return store.getState().categories.categories
}
})
Then I'm processing data to correct type
React.useEffect(() => {
if(info.length) {
const prods:any = []
info.forEach((product: any) => {
const productObj: any = {}
productObj._prodid = product?._id
productObj.image = product?.catalogProduct?.image
productObj.category = product?.catalogProduct?.category.name
productObj.name = product?.catalogProduct?.name
productObj.pricePerPiece = product?.catalogProduct?.pricePerPiece
productObj.pricePerPackage = product?.catalogProduct?.pricePerPackage
productObj.address = product?.address
productObj.piecesAtStorage = product?.piecesAtStorage
prods.push(productObj)
})
setData(prods)
}
}, [info])
It takes 3 re-renders.
First rerender - initial data of useState
Second rerender - initial data from useSelector
Third rerender - set data from useSelector into useState
And the output looks like this.
Is it possible to avoid rerender caused by useSelector?
Have you tried shallowEqual function?
import { shallowEqual, useSelector } from 'react-redux'
const selectedData = useSelector(selectorReturningObject, shallowEqual)
The problem is it returns a new array each time it runs. As for object and array, those with same properties/values are technically not same. This is why you are seeing same empty array twice.
You could combine the 2 things in one selector:
const { Provider, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const initialState = {
catalog: {
products: [{ _id: 1 }, { _id: 2 }],
categories: [{ _id: 3 }, { _id: 4 }],
},
};
const reducer = (state) => {
return state;
};
//selectors
const selectCatalog = (state) => state.catalog;
//select product or categories and map them
const selectData = createSelector(
[selectCatalog, (_, type) => type],
(catalog, type) => {
const data =
type === 'catalog'
? catalog.products
: type === 'category'
? catalog.categories
: [];
//use map instead of forEach
return data.map((item) => ({
//SO snippet has old babel so removed optional chaining
// you can put it back in your code
_prodid: item._id,
//you can figure out the other props
}));
}
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
() => (next) => (action) => next(action)
)
)
);
const App = () => {
const [type, setType] = React.useState('catalog');
const data = useSelector((state) =>
selectData(state, type)
);
console.log('render app', type);
return (
<div>
<select
value={type}
onChange={(e) => setType(e.target.value)}
>
<option value="catalog">catalog</option>
<option value="category">category</option>
</select>
<pre>{JSON.stringify(data, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
I created a custom hook
const useMessageStorage = () => {
const [messages, setMessages] = useState([]);
const addMessages = (newMessage) => {
setMessages(oldMessages => [...oldMessages, newMessage)]);
}
const clear = () => {
setMessages([]);
}
return { clear, addMessages }
}
Inside the consumer component, I want to do something like
const { clear, addMessages } = useMessageStorage();
...
clear() // clear before adding new messages
addMessage('new message')
The above doesn't work because both calls are asynchronous. But I want to clear all messages before adding a new message in some specific scenario. How to tackle this problem? I considered using useRef but I'm afraid of using it because of race condition.
React state updates are asynchronously processed, this is true, but they are also processed in the order they are enqueued in. Since your addMessages callback uses a functional state update it will update from the previous state, not the state of the render cycle it was enqueued in.
// clear()
setMessages([]); // "draft" state: []
// addMessage(newMessage)
setMessages((oldMessages) => [ // updated from "draft" state: [newMessage]
...oldMessages,
newMessage,
]);
Your code does work.
const useMessageStorage = () => {
const [messages, setMessages] = React.useState([]);
const addMessages = (newMessage) => {
setMessages((oldMessages) => [...oldMessages, newMessage]);
};
const clear = () => {
setMessages([]);
};
return { clear, messages, addMessages };
};
function App() {
const [message, setMessage] = React.useState("");
const { clear, messages, addMessages } = useMessageStorage();
const addMessage = () => {
if (message) {
addMessages(message);
setMessage("");
}
};
const clearAndAddMessage = () => {
if (message) {
clear();
addMessages(message);
setMessage("");
}
};
return (
<div className="App">
<label>
Message:
<input
type="text"
id="message"
onChange={(e) => setMessage(e.target.value)}
value={message}
/>
</label>
<button type="button" onClick={addMessage}>
Add Message
</button>
<button type="button" onClick={clearAndAddMessage}>
Clear & Add Message
</button>
<ul>
{messages.map((message, i) => (
<li key={i}>{message}</li>
))}
</ul>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root" />
I have a variable const foo: Array<ObjectExt> = useSelector(fooSelector); in a functional component. I want a copy of this variable from the first time the component is loaded that does not change when foo does.
When working with class components, I could simply have const fooCopy = foo.slice(); but that does not work here since the component reloads every time and fooCopy changes.
How do I achieve this in a functional component?
Just useState with the initial value as a copy of foo.
const foo : Array<ObjectExt> = useSelector(fooSelector);
const [origFoo] = useState(foo.slice());
Once origFoo has been initialized, it won't be re-initialized on rerender. You can destructure the setter out if you need to update its value later:
const [origFoo, setOrigFoo] = useState(foo);
// ...
if(someCondition) setOrigFoo(foo.slice())
const {useState} = React;
function App() {
const foo = [new Date().getTime()];
const [origFoo] = useState(foo.slice());
// Just so we have a way to force a rerender
const [count, setCount] = React.useState(0);
return (
<div>
<p>{JSON.stringify(foo)} </p>
<p>{JSON.stringify(origFoo)}</p>
<button onClick={() => setCount(count + 1)}>Update</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
One solution is to set a flag in local component state. If that flag is fals, then make a copy of the value. Otherwise, don't.
The solution I'd use to accomplish this functionality, is to make a copy of foo, something like initialFoo in the store, and pick it in needed components.
I want a copy of this variable from the first time the component is loaded that does not change when foo does.
When you use useSelector(selector) then react-redux will run selector every time the state changes, if the return value is different than then last time it ran then react-redux will re render the component.
The easiest way of doing this is using a selector that returns only the value it got when called the first time:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = { count: 0 };
//action types
const ADD = 'ADD';
//action creators
const add = () => ({
type: ADD,
});
const reducer = (state, { type, payload }) => {
if (type === ADD) {
return { ...state, count: state.count + 1 };
}
return state;
};
//selectors
const selectCount = (state) => state.count;
//function returning a function
const createSelectInitialCount = () => {
//initialize NONE when createSelectInitialCount is called
const NONE = {};
let last = NONE;
//return the selector
return (state) => {
//check if last value was set
if (last === NONE) {
//set last value (only when called the first time)
last = selectCount(state);
}
return last;
};
};
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const InitialFoo = React.memo(function InitialFoo(props) {
const selectInitialCount = React.useMemo(//can also use useCallback
createSelectInitialCount,//createSelectInitialCount() if you use useCallback
[]
);
const foo = useSelector(selectInitialCount);
return (
<div>
<h3>initialfoo</h3>
<pre>
{JSON.stringify({ ...props, foo }, undefined, 2)}
</pre>
</div>
);
});
const App = () => {
const foo = useSelector(selectCount);
const dispatch = useDispatch();
const [other, setOther] = React.useState(0);
const [showFoo, setShowFoo] = React.useState(true);
const remountFoo = () => {
setShowFoo(false);
Promise.resolve().then(() => setShowFoo(true));
};
return (
<div>
<button onClick={() => dispatch(add())}>
foo:{foo}
</button>
<button onClick={() => setOther((o) => o + 1)}>
other{other}
</button>
<button onClick={remountFoo}>remount Foo</button>
{showFoo && <InitialFoo other={other} />}
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>
Another way is to create a pure component using React.memo with a custom compare function that ignores foo:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = { count: 0 };
//action types
const ADD = 'ADD';
//action creators
const add = () => ({
type: ADD,
});
const reducer = (state, { type, payload }) => {
if (type === ADD) {
return { ...state, count: state.count + 1 };
}
return state;
};
//selectors
const selectCount = (state) => state.count;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const InitialFoo = React.memo(
function InitialFoo(props) {
return (
<div>
<h3>initialfoo</h3>
<pre>{JSON.stringify(props, undefined, 2)}</pre>
</div>
);
},
//custom compare function when returning false
// component will re render
({ other }, { other: newOther }) => {
return other === newOther;
}
);
const InitialFooContainer = (props) => {
const foo = useSelector(selectCount);
//store foo in ref, will never change after mount
const fooRef = React.useRef(foo);
const newProps = { ...props, foo: fooRef.current };
return <InitialFoo {...newProps} />;
};
const App = () => {
const foo = useSelector(selectCount);
const dispatch = useDispatch();
const [other, setOther] = React.useState(0);
const [showFoo, setShowFoo] = React.useState(true);
const remountFoo = () => {
setShowFoo(false);
Promise.resolve().then(() => setShowFoo(true));
};
return (
<div>
<button onClick={() => dispatch(add())}>
foo:{foo}
</button>
<button onClick={() => setOther((o) => o + 1)}>
other{other}
</button>
<button onClick={remountFoo}>remount Foo</button>
{showFoo && (
<InitialFooContainer foo={foo} other={other} />
)}
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>
Wrote a React Hooks component that renders randomly selected quotes from Redux.
I'm struggling to read the quote from props in the first render (had to write a switch statement in JSX to read from the local React component state on first render instead).
All fine to read from props when one clicks on the button to get a new random quote (i.e. much after the first render). Guess props is too slow to update.
Pointers on how to deal with it most welcome. What I've done is more of a hack as I'm not reading from the Redux store at all on first render.
const defaultState = {};
const NEW_QUOTE = "NEW_QUOTE"
const newQuoteActionCreator = (quoteObject) => {
return {
type: NEW_QUOTE,
payload: quoteObject
};
};
const getNextQuoteReducer = (state = defaultState, action) => {
switch (action.type) {
case NEW_QUOTE:
return {
...state,
data: action.payload
};
default:
return state;
}
};
const store = Redux.createStore(getNextQuoteReducer);
const quotes = [
{
quoteText:"\"AAAAAA.\"",
quoteAuthor:"BBBB",
},
{
quoteText:"\"CCCCC.\"",
quoteAuthor:"DDDD",
}
];
React bit:
const QuoteBox = ({ text, author }) => { //destructuring
return (
<React.Fragment>
<div className="quotable-square">
<div className="content">
<div id="text">{text}</div>
<div id="author" className="author">{author}</div>
</div>
</div>
</React.Fragment>
)
}
const Button = ({ onClick, title }) => {
return (
<button className="new-quote" onClick={onClick}>{title}</button>
)
}
const App = (props) => {
const [quote, setQuote] = React.useState(() => {
const initialQuote = quotes[Math.floor(Math.random() * quotes.length)];
return {
data: initialQuote
}
});
const chosenRandomQuoteToState = () => {
let chosenQuote = randomQuoteFunction(quotes);
props.selectNewQuote(chosenQuote);
}
return (
<React.Fragment>
<div className="container">
<div id="quote-box">
<QuoteBox text={(() => {
switch (typeof props.currentQuote.data) {
case "undefined": return quote.data.quoteText
default: return props.currentQuote.data.quoteText;
}
})()}/>
<div className="actions">
<Button id="new-quote" title="Get New Quote" onClick={chosenRandomQuoteToState} />
</div>
</div>
</div>
</React.Fragment>
)
}
React Redux bit:
const Provider = ReactRedux.Provider;
const mapStateToProps = (state) => {
return {
currentQuote: state
}
const mapDispatchToProps = (dispatch) => {
return {
selectNewQuote: function(quoteToBeNewQuote) {
dispatch(newQuoteActionCreator(quoteToBeNewQuote));
}
}
}
const connect = ReactRedux.connect
const Container = connect(mapStateToProps, mapDispatchToProps)(App);
const AppWrapper = () => {
return (
<Provider store= {store}>
<Container />
</Provider>
);
};
ReactDOM.render(<AppWrapper />, document.getElementById('app'));
Newbie, so be easy on me :)
Try something like this:
const initialState = {
quotes: [
{
quoteText: '"AAAAAA."',
quoteAuthor: 'BBBB',
},
{
quoteText: '"CCCCC."',
quoteAuthor: 'DDDD',
},
],
}; //renamed defaultState
const mapStateToProps = state => {
//state should hold data, container (map state) should
// create values based on that data
const { quotes } = state;
return {
currentQuote:
quotes[Math.floor(Math.random() * quotes.length)],
};
};
const mapDispatchToProps = dispatch => ({
selectNewQuote: dispatch(newQuoteActionCreator()),
});
const getNextQuoteReducer = (
state = defaultState,
action
) => {
switch (action.type) {
case NEW_QUOTE:
return {
...state,
quotes: [...state.quotes], //forces app container to re run
};
default:
return state;
}
};
And get rid of useState in your components as well as all complex logic with your action, the action only needs a type and your reducer will re refernce quites so App container will re run and pick another random quote.
Rather than messing with extra state in your component, you should go all-in with letting redux manage the state of the current quote. Since you want a random quote selected in your store at the very start, you could pre-select a random quote selection in your store's initial state or dispatch the selection of a random quote as an effect so it happens on mount. Here's the latter approach:
const App = (props) => {
useEffect(() => {
chosenRandomQuoteToState();
}, []);
const chosenRandomQuoteToState = () => {
let chosenQuote = randomQuoteFunction(quotes);
props.selectNewQuote(chosenQuote);
};
const currentQuote = props.currentQuote.data;
return (
<React.Fragment>
<div className="container">
<div id="quote-box">
{currentQuote && <QuoteBox text={currentQuote}/>}
<div className="actions">
<Button id="new-quote" title="Get New Quote" onClick={chosenRandomQuoteToState} />
</div>
</div>
</div>
</React.Fragment>
)
}