I'm now learning React and I have a problem with re-rendering component.
App.js code:
function App() {
const [expenses, setExpenses] = useState(INITIAL_EXPENSES);
const addNewExpenseHandler = (expense) => {
setExpenses((prevState) => {
return [expense, ...prevState];
}, changeYearHandler(filteredYear));
};
const filterExpenses = (expenses, year) => {
const newFilteredExpenses = expenses.filter((expense) => {
if (String(expense.date.getFullYear()) === year) {
return expense;
}
});
return newFilteredExpenses;
};
const [filteredYear, setFilteredYear] = useState('2019');
const [filteredExpenses, setFilteredExpenses] = useState(
filterExpenses(expenses, filteredYear)
);
const changeYearHandler = (value) => {
setFilteredYear(
value,
setFilteredExpenses(() => {
const newValue = filterExpenses(expenses, value);
return newValue;
})
);
};
return (
<>
<NewExpense onAddNewExpense={addNewExpenseHandler} />
<ExpenseFilter expenses={expenses} />
<ShowExpenses
onChangeYear={changeYearHandler}
data={filteredExpenses}
/>
</>
);
}
export default App;
The problem is that filteredExpenses isn't up-to-date. It's always retarded and it's the previous state. I was trying to call a changeYearHandler in callback of setExpenses and setFilteredExpense inside setFilteredYear but it's still doesn't work and I don't know why.
It feels like you're using a roundabout way to filter your expenses. What about just creating a memoized version of a filteredExpenses directly, using useMemo()?
const filteredExpenses = useMemo(() => {
return expenses.filter((expense) => {
if (String(expense.date.getFullYear()) === filteredYear) {
return expense;
}
});
}, [expenses, filteredYear]);
The dependency array will ensure that whenever expenses or filteredYear changes, then filteredExpenses will recompute and return a new filtered array (that is subsequently cached).
Related
I have an array of objects in my React state. I want to be able to map through them, find the one I need to update and update its value field. The body of my request being sent to the server should look like:
{ name: "nameOfInput", value:"theUserSetValue" type: "typeOfInput" }
What I thought would be simple is causing me some heartache. My reducer function calls, and I hit the "I AM RUNNING" log where it then jumps over my map and simply returns my state (which is empty). Please note that I NEVER see the "I SHOULD RETURN SOMETHING BUT I DONT" log.
NOTE: I have learned that I could be simply handingling this with useState
function Form(props) {
const title = props.title;
const paragraph = props.paragraph;
const formBlocks = props.blocks.formBlocks
const submitEndpoint = props.blocks.submitEndpoint || "";
const action = props.blocks.action || "POST";
const formReducer = (state, e) => {
console.log("I AM RUNNING")
state.map((obj) => {
console.log("I SHOULD RETURN SOMETHING BUT I DONT")
if (obj.name === e.target.name) {
console.log("OBJ EXISTS", obj)
return {...obj, [e.target.name]:obj.value}
} else {
console.log("NO MATCH", obj)
return obj
}
});
return state
}
const [formData, setFormData] = useReducer(formReducer, []);
const [isSubmitting, setIsSubmitting] = useState(false);
=====================================================================
Where I am calling my reducer from:
<div className="form-block-wrapper">
{formBlocks.map((block, i) => {
return <FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={setFormData}
/>
})}
</div>
Issues
When using the useReducer hook you should dispatch actions to effect changes to the state. The reducer function should handle the different cases. From what I see of the code snippet it's not clear if you even need to use the useReducer hook.
When mapping an array not only do you need to return a value for each iterated element, but you also need to return the new array.
Solution
Using useReducer
const formReducer = (state, action) => {
switch(action.type) {
case "UPDATE":
const { name, value } = action.payload;
return state.map((obj) => obj.name === name
? { ...obj, [name]: value }
: obj
);
default:
return state;
}
};
...
const [formData, dispatch] = useReducer(formReducer, []);
...
{formBlocks.map((block, i) => {
return (
<FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={e => dispatch({
type: "UPDATE",
payload: {...e.target}
})}
/>
);
})}
Using useState
const [formData, setFormData] = useState([]);
...
const changeHandler = e => {
const { name, value } = e.target;
setFormData(data => data.map(obj => obj.name === name
? { ...obj, [name]: value }
: obj
));
};
...
{formBlocks.map((block, i) => {
return (
<FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={changeHandler}
/>
);
})}
I have come to understand my problem much better now and I'll update my question to reflect this.
As the user interacted with an input I needed to figure out if they had interacted with it before
If they did interact with it before, I needed to find that interaction in the state[] and update the value as required
If they didn't I needed to add an entirely new object to my forms state[]
I wrote two new functions, an AddObjectToArray function and an UpdateObjectInArray function to serve these purposes.
const handleFormInputChange = (e) => {
const { name, value, type } = e.target;
const addObjectToArray = (obj) => {
console.log("OBJECT TO BE ADDED TO ARRAY:", obj)
setFormData(currentArray => ([...currentArray, obj]))
}
const updateObjectInArray = () => {
const updatedObject = formData.map(obj => {
if (obj.name === name) {
//If the name matches, Update the value of the input
return ({...obj, value:value})
}
else {
//if no match just return the object as is
return obj
}
})
setFormData(updatedObject)
}
//Check if the user has already interacted with this input
if (formData.find(input => input.name === name)) {
updateObjectInArray()
}
else {
addObjectToArray({name, value, type})
}
}
I could get more complicated with this now and begin to write custom hooks that take a setState function as a callback and the data to be handled.
I'm trying to re render only when the minutes change using React.memo() like this:
function getCurrentTime(){
let now = new Date();
return ({
'mins': now.getMinutes(),
'secs': now.getSeconds()
})
}
const Disp = React.memo(({ timeObj }) => { //this Component is suppose to be in another file
return (<Text>{timeObj['mins']}</Text>);
}, (prevProp, newProp) => {
if (prevProp['mins'] == newProp['mins'])
return false;
return true;
});
export default function App() {
const [CurrentTime, setCurrentTime] = useState(() => getCurrentTime());
useEffect(() => {
let secTimer = setInterval(() => {setCurrentTime(getCurrentTime())}, 500);
return () => { clearInterval(secTimer) };
}, []);
return (
<View style={styles.body}>
<Disp timeObj={CurrentTime} />
</View>
);
}
but for some reason it isn't working & renders every 500 ms
I've followed this tutorial
You have your return values backward in your comparison function. From the documentation (in a comment in the code sample):
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
You're doing the opposite, returning false when the minutes are the same.
Also, you're missing out the timeObj part (thanks Felix!), it should be prevProps.timeObj.mins (and the same for newProps). (Also, "props" should be plural, and generally best to write .mins rather than ['mins'].)
Instead:
const Disp = React.memo(
({ timeObj }) => { //this Component is supposed to be in another file
return (<Text>{timeObj.mins}</Text>);
},
(prevProps, newProps) => {
// Return true if the props are the same for rendering purposes,
// false if they aren't
return prevProps.timeObj.mins == newProps.timeObj.mins;
}
);
As a side note, you can use nested destructuring if all you want is the mins from timeObj (you can do that both in the component and the comparison function, but I'd probably only do it in the component, gets confusing doing the renaming needed):
const Disp = React.memo(
({ timeObj: { mins } }) => { //this Component is supposed to be in another file
return (<Text>{mins}</Text>);
},
(prevProps, newProps) => {
// Return true if the props are the same for rendering purposes,
// false if they aren't
return prevProps.timeObj.mins == newProps.timeObj.mins;
}
);
I have the following context:
import React, { createContext, useState } from "react";
const OtherUsersContext = createContext(null);
export default OtherUsersContext;
export function OtherUsersProvider({ children }) {
const [otherUsers, setOtherUsers] = useState(new Map([]));
const addUser = (userId, userData) => {
setOtherUsers(
(prevOtherUsers) => new Map([...prevOtherUsers, [userId, userData]])
);
};
const updateUser = (userId, userData, merge = true) => {
...
};
const getUser = (userId) => otherUsers.get(userId);
const resetUsers = () => {
setOtherUsers(new Map([]));
};
return (
<OtherUsersContext.Provider
value={{
addUser,
updateUser,
getUser,
resetUsers,
}}
>
{children}
</OtherUsersContext.Provider>
);
}
In my app, when a user signs out, I need to reset this context's map, using the function "resetUsers".
Currently this is working good, but there has no sense to reset the map if it has no values, so I have changed the "resetUsers" function to:
const resetUsers = () => {
if(otherUsers.size) {
setOtherUsers(new Map([]));
}
}
And, this is not working good, because inside resetUsers, otherUsers.size is always 0. Something which disturbs me because outside the function, the value is the correct one...
...
const resetUsers = () => {
console.log(otherUsers.size); // 0
setOtherUsers(new Map([]));
};
console.log(otherUsers.size); // 5
return ( ...
Any ideas?
The functional updates part of the hooks docs. says:
If the new state is computed using the previous state, you can pass a function to setState.
So instead of just passing the new value to your setter, you can pass a function that depends on the previous state.
This means that you can do:
const resetUsers = () => {
setOtherUsers(prevOtherUsers => prevOtherUsers.size ? new Map([]): prevOtherUsers);
}
One tip, if you are not getting the most updated state value inside a function, then wrap it inside an useCallback.
Try this:
const resetUsers = useCallback(() => {
if (otherUsers.size > 0) {
console.log(otherUsers.size); // 5
setOtherUsers(new Map([]));
}
}, [otherUsers]);
guys i wanna convert this code:
export default class App extends Component {
constructor(props) {
super(props);
this.state = { isLoading: true };
}
performTimeConsumingTask = async () => {
return new Promise((resolve) =>
setTimeout(() => {
resolve('result');
}, 2000)
);
};
async componentDidMount() {
const data = await this.performTimeConsumingTask();
if (data !== null) this.setState({ isLoading: false });
}
render() {
if (this.state.isLoading) return <SplashScreen />;
const { state, navigate } = this.props.navigation;
return (something)
i wrote this code but it doesn`t work :
const App = () => {
const [fontLoaded, setFontLoaded] = useState(false);
const [isTimerOn, setIsTimerOn] = useState(true);
if (!fontLoaded) {
return (
<AppLoading
startAsync={fetchFonts}
onFinish={() => setFontLoaded(true)}
/>
);
}
useEffect(async () => {
const data = await performTimeConsumingTask();
if (data !== null) setIsTimerOn(false);
});
if (isTimerOn) return <SplashScreen />;
else {
return (something)
This will show an error :
Invariant Violation: Rendered More Hooks than during the previous render.
If I comment the useEffect hook it will run the splashScreen. Can any one help me in converting it?
Pass [] as an argument if you wanted to use this hook as componentDidMount
useEffect(async () => {
const data = await performTimeConsumingTask();
if (data !== null) setIsTimerOn(false);
}, []);
Here is a list of hooks how you can use hooks to replace lifecycle methods
https://medium.com/javascript-in-plain-english/lifecycle-methods-substitute-with-react-hooks-b173073052a
The Reason for getting an error is your component is rendering too many times and useEffect is also running on each render by passing [] will run the useEffect on first render as it will behave like componentDidMount.
Also follow this to make network calls inside useEffect
https://medium.com/javascript-in-plain-english/handling-api-calls-using-async-await-in-useeffect-hook-990fb4ae423
There must be no conditional return before using all the hooks, in your case you return before using useEffect.
Also useEffect must not run on every render since it sets state in your case. Since you only want it to run on initial render pass an empty array as the second argument.
Also useEffect callback function cannot be async.
Read more about useEffect hook in the documentation.
Check updated code below
const App = () => {
const [fontLoaded, setFontLoaded] = useState(false);
const [isTimerOn, setIsTimerOn] = useState(true);
const performTimeConsumingTask = async () => {
return new Promise((resolve) =>
setTimeout(() => {
resolve('result');
}, 2000)
);
};
useEffect(() => {
async function myFunction() {
const data = await performTimeConsumingTask();
if (data !== null) setIsTimerOn(false);
}
myFunction();
}, []); // With empty dependency it runs on initial render only like componentDidMount
if (!fontLoaded) {
return (
<AppLoading
startAsync={fetchFonts}
onFinish={() => setFontLoaded(true)}
/>
);
}
if (isTimerOn) return <SplashScreen />;
else {
return (something)
In my Typescript app, there's a class that represents some data. This class is being shared end to end (both front-and-back ends use it to structure the data). It has a property named items which is an array of numbers.
class Data {
constructor() {
this.items = [0];
}
addItem() {
this.items = [...this.items, this.items.length];
}
}
I'm trying to render those numbers in my component, but since modifying the class instance won't cause a re-render, I have to "force rerender" to make the new items values render:
const INSTANCE = new Data();
function ItemsDisplay() {
const forceUpdate = useUpdate(); // from react-use
useEffect(() => {
const interval = setInterval(() => {
INSTANCE.addItem();
forceUpdate(); // make it work
}, 2000);
return () => clearInterval(interval);
}, []);
return (
<div>
<h1>with class:</h1>
<div>{INSTANCE.items.map(item => <span>{item}</span>)}</div>
</div>
);
}
While this works, it has one major drawback: addItem() is not the only modification done to INSTANCE; This class has actually around 10 to 15 properties that represent different data parts. So, doing forceUpdate() wherever a modification happens is a nightmare. Not no mention, if this instance will be modified outside the component, I won't be able to forceUpdate() to sync the change with the component.
Using useState([]) to represent items will solve this issue, but as I said Data has a lot of properties, so as some functions. That's another nightmare.
I would like to know what's the best way of rendering data from a class instance, without rerender hacks or unpacking the whole instance into a local component state.
Thanks!
Here's a Codesandbox demo that shows the differences between using a class and a local state.
Here is an example of how you can make Data instance observable and use Effect in your components to observe changes in Data instance items:
const { useState, useEffect } = React;
class Data {
constructor() {
this.data = {
users: [],
products: [],
};
this.listeners = [];
}
addItem(type, newItem) {
this.data[type] = [...this.data[type], newItem];
//notify all listeners that something has been changed
this.notify();
}
addUser(user) {
this.addItem('users', user);
}
addProduct(product) {
this.addItem('products', product);
}
reset = () => {
this.data.users = [];
this.data.products = [];
this.notify();
};
notify() {
this.listeners.forEach((l) => l(this.data));
}
addListener = (fn) => {
this.listeners.push(fn);
//return the remove listener function
return () =>
(this.listeners = this.listeners.filter(
(l) => l !== fn
));
};
}
const instance = new Data();
let counter = 0;
setInterval(() => {
if (counter < 10) {
if (counter % 2) {
instance.addUser({ userName: counter });
} else {
instance.addProduct({ productId: counter });
}
counter++;
}
}, 500);
//custom hook to use instance
const useInstance = (instance, fn = (id) => id) => {
const [items, setItems] = useState(fn(instance.data));
useEffect(
() =>
instance.addListener((items) => setItems(fn(items))),
[instance, fn]
);
return items;
};
const getUsers = (data) => data.users;
const getProducts = (data) => data.products;
const Users = () => {
const users = useInstance(instance, getUsers);
return <pre>{JSON.stringify(users)}</pre>;
};
const Products = () => {
const products = useInstance(instance, getProducts);
return <pre>{JSON.stringify(products)}</pre>;
};
const App = () => {
const reset = () => {
instance.reset();
counter = 0;
};
return (
<div>
<button onClick={reset}>Reset</button>
<div>
users:
<Users />
</div>
<div>
products:
<Products />
</div>
</div>
);
};
ReactDOM.render(<App />, 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>
<div id="root"></div>