GSAP animate elements in an array fetched from the server - reactjs

I would like to animate each element in an array with TweenMax.staggerFrom. At the moment I created this working sample
import React, { useRef, useEffect createRef } from 'react';
import { TweenMax } from 'gsap';
const AnimateView = () => {
const data = [
{ title: 'Title 1', value: 1000 },
{ title: 'Title 2', value: 1100 },
{ title: 'Title 3', value: 1200 },
];
const elementsRef = useRef(data.map(() => createRef()));
useEffect(() => {
const elements = elementsRef.current.map(el => el.current);
TweenMax.staggerFrom(elements, 1, { scale: 0 }, 0.3);
}, []);
return (
<>
{data.map((item, index) => (
<div ref={elementsRef.current[index]}>{item.title}</div>
))}
</>
);
};
export default AnimateView;
The difference is that I want data array to be fetched from the server. I cannot figure out why in elementsRef I'm getting no attached refs. Below you can check what I wanted to achieve.
import React, { useRef, useEffect createRef } from 'react';
import { TweenMax } from 'gsap';
import { connect } from 'react-redux';
import { fetchData } from 'actions';
const AnimateView = ({ combineFetching, income }) => {
const elementsRef = useRef(income.length && income.map(() => createRef()));
useEffect(() => {
const elements = elementsRef.current.map(el => el.current);
TweenMax.staggerFrom(elements, 1, { scale: 0 }, 0.3);
}, []);
return (
<>
<button onClick={fetchData}>Click</button>
{income.map((item, index) => (
<div ref={elementsRef.current[index]}>{item.title}</div>
))}
</>
);
};
const mapDispatchToProps = state => ({
income: state.budget.income,
});
export default connect(
mapDispatchToProps,
{ fetchData },
)(AnimateView);
On Click I want to fetch data from database and animate each element in the array.

Related

React calls api thousands of times inside a useEffect

I am building a ToDoList with React and a Django rest Api but I am also using a Datepicker to render all the tasks for the day by the date created. But every time the site loads the api is called thousands of times. The goal would be to only render the tasks for the specific day when the date is changed or a new task for the day is added and not call the backend constalnly the problem seams to lay at the ToDoList because evertwhere else the props.date is only called once and not in a loop.
import React, { useState } from 'react'
import 'date-fns'
import Grid from '#material-ui/core/Grid'
import DateFnsUtils from '#date-io/date-fns'
import{
MuiPickersUtilsProvider,
KeyboardTimePicker,
KeyboardDatePicker
} from '#material-ui/pickers'
import TodoForm from '../ToDo/TodoForm'
function Datepicker() {
const initialDate = new Date(Date.now())
const [selectDate, setSelectDate] = useState(
`${initialDate.getFullYear()}-${initialDate.getMonth()+1}-${initialDate.getDate()}`
)
const handleDateChange = (date) =>{
setSelectDate(`${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()}`)
}
return (
<div>
<div>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<Grid container justify='space-around'>
<KeyboardDatePicker
disableToolbar
varient='inline'
format='MM/dd/yy'
margin='normal'
id='date-picker'
label='Pick your Date'
value={selectDate}
onChange={handleDateChange}
KeyboradButtonProps={{
'aris-label': 'change date'
}}
/>
</Grid>
</MuiPickersUtilsProvider>
</div>
<div>
<TodoForm date={selectDate}/>
</div>
</div>
)
}
export default Datepicker
import React, { Component, useState,useEffect } from 'react'
import Select from 'react-select'
import { apiTaskCreate } from './lookup'
import { ActionBtn } from './buttons'
import TodoList from './TodoList'
function TodoForm(props) {
const [newTasks, setNewTasks] = useState([])
const [taskname, SetTaskname] = useState('')
const [Importants, setImportants] = useState({})
const [TimeComplete, setTimeComplete] = useState({})
const handleChange = e => {
SetTaskname(e.target.value)
}
function onChangeImportants(value){
setImportants(value.value)
}
function onChangeTimeComplete(value){
setTimeComplete(value.value)
}
const handleSubmit = e =>{
e.preventDefault()
SetTaskname('')
let tempNewTasks = [...newTasks]
apiTaskCreate(taskname,Importants,TimeComplete,(response, status)=>{
// console.log(response, status)
if (status === 201){
tempNewTasks.unshift(response)
setNewTasks(tempNewTasks)
} else {
console.log(response)
alert("an error accourd")
}
})
}
const Importants_options = [
{ value: '1', label: 1 },
{ value: '2', label: 2 },
{ value: '3', label: 3 },
{ value: '4', label: 4 },
{ value: '5', label: 5 },
]
const Time_options = [
{ value: '1', label: 30 },
{ value: '2', label: 60 },
{ value: '3', label: 90 },
{ value: '4', label: 120 },
{ value: '5', label: 150 },
{ value: '6', label: 180 },
]
return (
<div className={props.className}>
<div className='col-11 mb-3'>
<form className='todo-form mb-3' onSubmit={handleSubmit}>
<input type='text' value={taskname} placeholder='Task Name'
name='task_name' className='todo-input' onChange={handleChange}></input>
<Select onChange={onChangeImportants} options={Importants_options} placeholder="Importants Score"/>
<Select onChange={onChangeTimeComplete} options={Time_options} placeholder="Time to complete"/>
<button className='btn btn-primary'>Submit</button>
<ActionBtn action={{type: 'optimize', display:"Optimize"}}/>
</form>
</div>
<div className='container'>
<TodoList newTasks={newTasks} {...props}/>
</div>
</div>
)
}
export default TodoForm
import React, {useState, useEffect} from 'react'
import { apiTaskList } from './lookup'
import Task from './Task'
function TodoList(props) {
const [tasksInit, setTasksInit] = useState([])
const [tasks, setTasks] = useState([])
const [tasksDidSet, setTasksDidSet] = useState(false)
const initialDate = new Date(Date.now())
const [date, setDate] = useState("2021-04-12")
// `${initialDate.getFullYear()}-${initialDate.getMonth()+1}-${initialDate.getDate()}`
useEffect( () =>{
const final = [...props.newTasks].concat(tasksInit)
if (final.length !== tasks.length) {
setTasks(final)
}
}, [props.newTasks, tasks, tasksInit])
useEffect(() => {
if (tasksDidSet === false) {
const handleTasksListLookup = (response, status) => {
if (status === 200) {
setTasksInit(response)
setDate(props.date)
console.log(date)
}
}
apiTaskList("admin", date ,handleTasksListLookup)
}
}, [tasksInit, setTasksDidSet, setTasksDidSet, date])
return tasks.map((item, index)=>{
return <Task task={item} className='d-flex p-2 justify-content-between border bg-white text-dark' key={`${index}-${item.id}`}/>
})
}
export default TodoList;
import { backendlookup } from "../lookup/lookup";
export function apiTaskCreate(newTask_Name,newImportans_Score,newTime_to_complete,callback) {
backendlookup('POST', 'create',callback, {
Task_name: newTask_Name,
Importants_Score: newImportans_Score,
Time_to_Finish: newTime_to_complete,
})
}
export function apiTaskList(username,date,callback) {
let endpoint = 'tasks'
if (date){
endpoint = `tasks?username=${username}&date=${date}`
}
backendlookup('GET', endpoint ,callback)
}
export function apiPartyActionOptimize(action,callback) {
backendlookup('POST', 'action-optimize',callback, {action:action})
You are adding a dependency to useEffect which itself is being updated inside it. If you do so, your useEffect execution will go into an infinite loop.
You can make use of functional version of setState to update the tasks state.
Your updated code will look as follows
useEffect( () =>{
const final = [...props.newTasks].concat(tasksInit);
setTasks(tasks => {
if (final.length !== tasks.length) {
return final;
}
return tasks
});
}, [props.newTasks, tasksInit])
useEffect(() => {
if (tasksDidSet === false) {
const handleTasksListLookup = (response, status) => {
if (status === 200) {
setTasksInit(response);
setDate(props.date);
}
}
apiTaskList("admin", date ,handleTasksListLookup)
}
}, [setTasksInit, props.date, tasksDidSet])
When you call the setTasks method inside the useEffect function it updates the tasks value and because you have the tasks variable as one of the dependencies in the useEffect callback, the API gets called indefinitely. Remove tasks from the useEffect dependency and it should work.

Unable to re-render FlatList in Functional Components?

I want to re-render this flat list whenever I delete an item, I have tried several methods but nothing works, also I am getting an error:
Hooks can only be used in functional components
import React, { useEffect,useState } from "react";
import { Alert, FlatList, StyleSheet } from "react-native";
import { SearchBar } from 'react-native-elements';
import Swipeout from 'react-native-swipeout';
import ReactDOM from "react-dom";
import ActivityIndicator from "../components/ActivityIndicator";
import Button from "../components/Button";
import Card from "../components/Card";
import colors from "../config/colors";
import listingsApi from "../api/listings";
import routes from "../navigation/routes";
import Screen from "../components/Screen";
import AppText from "../components/Text";
import useApi from "../hooks/useApi";
import filter from 'lodash.filter';
import useAuth from "../auth/useAuth";
import client from "../api/client"
// function useForceUpdate()
// {
// let[value, setState]=useState(true);
// return () => setState(!value);
}
function ListingsScreen({ navigation }) {
//let forceUpdate=useForceUpdate();
const getListingsApi = useApi(listingsApi.getListings);
const { user, logOut } = useAuth();
const [filteredDataSource, setFilteredDataSource] = useState([]);
useEffect(() => {
fetch('http://192.168.1.6:9000/api/listings')
.then((response) => response.json())
.then((responseJson) => {
setFilteredDataSource(responseJson);
//filteredDataSource(responseJson)
// console.log(responseJson)
})
.catch((error) => {
console.error(error);
alert("Couldn't retrieve the listings.")
});
}, []);
var data =[];
const searchFilterFunction = () => {
const newData = filteredDataSource.filter(function (item) {
const itemData = user.email
const itemd=item.emailuser
return(itemd === itemData)
});
//setFilteredDataSource(newData);
//searchFilterFunction();
data=newData;
}
searchFilterFunction();
return (
<Screen style={styles.screen}>
{getListingsApi.error&& (
<>
<AppText>Couldn't retrieve the listings.</AppText>
<Button title="Retry" onPress={getListingsApi.request} />
</>
)}
<ActivityIndicator visible={getListingsApi.loading} />
{
searchFilterFunction()
}
<FlatList
data={data}
keyExtractor={(data) => data._id.toString()}
renderItem={({ item,index }) => {
// ------delete function -----//
const handlePress = (userId) => {
alert("Listing Deleted Succesfully")
console.log("del")
client.post("/listings/delete", {userId})
}
const swipeSettings = {
autoClose :true,
onClose: (secId, rowId, direction) => {
},
onOpen: (secId, rowId, direction) => {
},
right: [
{
onPress: () => {
Alert.alert(
'Alert',
'Are you sure you want to delete ?',
[
{ text : 'No', onPress: () => console.log('Cancel Pressed'),style : 'cancel'},
{ text : 'Yes', onPress:() => handlePress(item._id) }
]
);
},
text: 'Delete', type : 'delete',
}
],
// rowId: this.index,
// sectionId: 1
};
return(
<Swipeout {...swipeSettings} backgroundColor="transparent" style={styles.btn}>
<Card
title={item.title}
subTitle={"$" + item.price}
category={item.categoryId}
imageUrl={item.image}
onPress={() => navigation.navigate(routes.LISTING_DETAILS, item)}
/>
</Swipeout>);
}}
/>
</Screen>
);
}
const styles = StyleSheet.create({
screen: {
padding: 5,
backgroundColor: colors.light,
},
btn: {
flex: 1,
}
});
export default ListingsScreen;
Thanks in advance.

DetailsList is getting re-rendered and hence is loosing its state

So I have been trying to accomplish the idea that whenever the user clicks any row in the DetailsList the Button gets enabled and when the user clicks outside the selectionzone the Button gets disabled.
This is my code
import { DetailsList, SelectionMode, Selection, ISelection, initializeIcons, PrimaryButton } from '#fluentui/react'
import {useMemo } from 'react'
import { useBoolean } from '#uifabric/react-hooks'
interface ICurrency {
type: string,
amount: number
}
function App() {
initializeIcons()
const [isBtn, { setTrue: disableBtn, setFalse: enableBtn }] = useBoolean(true)
const items: ICurrency[] = [
{
type: 'INR',
amount: 20
},
{
type: 'USD',
amount: 50
},
{
type: 'GBP',
amount: 70
}
]
const selection: ISelection = useMemo(() => new Selection(
{
onSelectionChanged: ()=>{
if(selection.getSelectedCount() > 0){
enableBtn()
}else{
disableBtn()
}
}
}
), [items])
return (
<div className="App">
<PrimaryButton text="Button" disabled={isBtn}/>
<DetailsList
items={items} selectionMode={SelectionMode.single}
selection={selection}
/>
</div>
);
}
export default App;
I even used useMemo and kept on banging my head but the problem persists where clicking any row the state is lost and the button is not enabled. I have already tried storing the state of selection also, count, everything but it seems I'm missing out on something essential or fundamental for the implementation
You need to memoize items because on each render it's being re-assigned and considered a new array which causes your selection to change because it relies on items as a useMemo dependency. On each state update the selection will reset.
So one way you can fix this is by moving the items out of the function so that it holds reference instead of creating a new items array on each render.
const items = [
{
type: "INR",
amount: 20
},
{
type: "USD",
amount: 50
},
{
type: "GBP",
amount: 70
}
];
function App() {
// code
}
or by using useMemo on those items:
const items = useMemo(() => [
{
type: "INR",
amount: 20
},
{
type: "USD",
amount: 50
},
{
type: "GBP",
amount: 70
}
],[]);
Also I see you have an error, the initializeIcons should only be called once. So that should probably be placed in useEffect:
useEffect(() => {
initializeIcons();
},[])
The final code sample should look like this:
import {
DetailsList,
SelectionMode,
Selection,
ISelection,
initializeIcons,
PrimaryButton
} from "#fluentui/react";
import { useMemo, useEffect } from "react";
import { useBoolean } from "#uifabric/react-hooks";
const items = [
{
type: "INR",
amount: 20
},
{
type: "USD",
amount: 50
},
{
type: "GBP",
amount: 70
}
];
function App() {
useEffect(() => {
initializeIcons();
}, []);
const [isBtn, { setTrue: disableBtn, setFalse: enableBtn }] = useBoolean(
true
);
const selection = useMemo(
() =>
new Selection({
onSelectionChanged: () => {
if (selection.getSelectedCount() > 0) {
enableBtn();
} else {
disableBtn();
}
}
}),
[items]
);
return (
<div className="App">
<PrimaryButton text="Button" disabled={isBtn} />
<DetailsList
items={items}
selectionMode={SelectionMode.single}
selection={selection}
/>
</div>
);
}
export default App;
The Accepted answer has the proper reasoning for the issue. I just wanted to post my solution too. I just had to store the state of the items
import { DetailsList, SelectionMode, Selection, initializeIcons, PrimaryButton } from '#fluentui/react'
import { useEffect, useState } from 'react'
import { useBoolean } from '#uifabric/react-hooks'
interface ICurrency {
type: string,
amount: number
}
function App() {
useEffect(() => {
initializeIcons();
}, []);
const [isBtn, { setTrue: disableBtn, setFalse: enableBtn }] = useBoolean(true)
let _selection = new Selection({
onSelectionChanged: () => {
if (_selection.getSelectedCount() > 0) {
enableBtn()
} else {
disableBtn()
}
}
});
let _initialItems: ICurrency[] = [
{
type: 'INR',
amount: 20
},
{
type: 'USD',
amount: 50
},
{
type: 'GBP',
amount: 70
}
]
const [items, setItems] = useState(_initialItems)
return (
<>
<PrimaryButton text="Button" disabled={isBtn} />
<DetailsList
items={items}
selection={_selection}
selectionMode={SelectionMode.single}
/>
</>
);
}
export default App;
Now say if the items are coming from some props or some state management, then just use setItems inside a useEffect and set the dependency as that source

Problem with Re-rendering when passing a React function with React Context API

I have a simple example where I pass a clickFunction as a value to React Context and then access that value in a child component. That child component re-renders event though I'm using React.memo and React.useCallback. I have an example in stackblitz that does not have the re-render problem without using context here:
https://stackblitz.com/edit/react-y5w2cp (no problem with this)
But, when I add context and pass the the function as part of the value of the context, all children component re-render. Example showing problem here:
https://stackblitz.com/edit/react-wpnmuk
Here is the problem code:
Hello.js
import React, { useCallback, useState, createContext } from "react";
import Speaker from "./Speaker";
export const GlobalContext = createContext({});
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = useCallback((speakerIdClicked) => {
setSpeakers((currentState) =>
currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
}, []);
return (
<GlobalContext.Provider
value={{
clickFunction: memoizedValue,
}}
>
{speakers.map((rec) => {
return <Speaker speaker={rec} key={rec.id}></Speaker>;
})}
</GlobalContext.Provider>
);
};
Speaker.js
import React, {useContext} from "react";
import { GlobalContext } from "./Hello";
export default React.memo(({ speaker }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
const { clickFunction } = useContext(GlobalContext);
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
WORKING CODE BELOW FROM ANSWERS BELOW
Speaker.js
import React, { useContext } from "react";
import { GlobalContext } from "./Hello";
export default React.memo(({ speaker }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
const { clickFunction } = useContext(GlobalContext);
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
Hello.js
import React, { useState, createContext, useMemo } from "react";
import Speaker from "./Speaker";
export const GlobalContext = createContext({});
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = (speakerIdClicked) => {
setSpeakers((currentState) =>
currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
};
const provider = useMemo(() => {
return ({clickFunction: clickFunction});
}, []);
return (
<GlobalContext.Provider value={provider}>
{speakers.map((rec) => {
return <Speaker speaker={rec} key={rec.id}></Speaker>;
})}
</GlobalContext.Provider>
);
};
when passing value={{clickFunction}} as prop to Provider like this when the component re render and will recreate this object so which will make child update, so to prevent this
you need to memoized the value with useMemo.
here the code:
import React, { useCallback, useState, createContext,useMemo } from "react";
import Speaker from "./Speaker";
export const GlobalContext = createContext({});
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = useCallback((speakerIdClicked) => {
setSpeakers((currentState) =>
currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
}, []);
const provider =useMemo(()=>({clickFunction}),[])
return (
<div>
{speakers.map((rec) => {
return (
<GlobalContext.Provider value={provider}>
<Speaker
speaker={rec}
key={rec.id}
></Speaker>
</GlobalContext.Provider>
);
})}
</div>
);
};
note you dont need to use useCallback anymore clickFunction
This is because your value you pass to your provider changes every time. So, this causes a re-render because your Speaker component thinks the value is changed.
Maybe you can use something like this:
const memoizedValue = useMemo(() => ({ clickFunction }), []);
and remove useCallback from the function definition since useMemo will handle this part for you.
const clickFunction = speakerIdClicked =>
setSpeakers(currentState =>
currentState.map(rec => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
and pass this to your provider such as:
<GlobalContext.Provider value={memoizedValue}>
<Speaker speaker={rec} key={rec.id} />
</GlobalContext.Provider>
After providing the answer, I've realized that you are using Context somehow wrong. You are mapping an array and creating multiple providers for each data. You should probably change your logic.
Update:
Most of the time you want to keep the state in your context. So, you can get it from the value as well. Providing a working example below. Be careful about the function this time, we are using useCallback for it to get a stable reference.
const GlobalContext = React.createContext({});
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
function App() {
const [speakers, setSpeakers] = React.useState(speakersArray);
const clickFunction = React.useCallback((speakerIdClicked) => {
setSpeakers((currentState) =>
currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
}, []);
const memoizedValue = React.useMemo(() => ({ speakers, clickFunction }), [
speakers,
clickFunction,
]);
return (
<GlobalContext.Provider value={memoizedValue}>
<Speakers />
</GlobalContext.Provider>
);
}
function Speakers() {
const { speakers, clickFunction } = React.useContext(GlobalContext);
return speakers.map((speaker) => (
<Speaker key={speaker.id} speaker={speaker} clickFunction={clickFunction} />
));
}
const Speaker = React.memo(({ speaker, clickFunction }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" />

Problem with re-renders in React with useCallback and useMemo

I've got a fairly simple example of a component (Hello.js) that renders three components, each with a different id (Speaker.js). I have a clickFunction that I pass back from the Speaker.js. I would think that using React.memo and React.useCallback would stop all three from re-rendering when only one changes, but sadly, you can see from the console.log in Speaker.js, clicking any of the three buttons causes all three to render.
Here is the problem example on stackblitz:
https://stackblitz.com/edit/react-dmclqm
Hello.js
import React, { useCallback, useState } from "react";
import Speaker from "./Speaker";
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = useCallback((speakerIdClicked) => {
var speakersArrayUpdated = speakers.map((rec) => {
if (rec.id === speakerIdClicked) {
rec.favorite = !rec.favorite;
}
return rec;
});
setSpeakers(speakersArrayUpdated);
},[speakers]);
return (
<div>
{speakers.map((rec) => {
return (
<Speaker
speaker={rec}
key={rec.id}
clickFunction={clickFunction}
></Speaker>
);
})}
</div>
);
};
Speaker.js
import React from "react";
export default React.memo(({ speaker, clickFunction }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
because when you fire clickFunction it update speakers wich cause the recreating of this functions, to solve this you need to remove speakers from clickFunction dependencies and accessing it from setState callback.
here the solution :
import React, { useCallback, useState,useEffect } from "react";
import Speaker from "./Speaker";
export default () => {
const [speakers, setSpeakers] = useState([
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
]);
const clickFunction = useCallback((speakerIdClicked) => {
setSpeakers(currentState=>currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
rec.favorite = !rec.favorite;
return {...rec};
}
return rec
}));
},[]);
useEffect(()=>{
console.log("render")
})
return (
<div>
{speakers.map((rec) => {
return (
<Speaker
speaker={rec}
key={rec.id}
clickFunction={clickFunction}
></Speaker>
);
})}
</div>
);
};
and for speaker component:
import React from "react";
export default React.memo(({ speaker, clickFunction }) => {
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
Upon further reflection, I think my answer may not be entirely correct: without the [speakers] dependency this won't work as intended.
Two things:
The [speakers] dependency passed to useCallback causes the function to get recreated every time speakers changes, and because the callback itself calls setSpeakers, it will get recreated on every render.
If you fix #1, the Speaker components won't re-render at all, because they're receiving the same speaker prop. The fact that speaker.favorite has changed doesn't trigger a re-render because speaker is still the same object. To fix this, have your click function return a copy of rec with favorite flipped instead of just toggling it in the existing object:
import React, { useCallback, useState } from "react";
import Speaker from "./Speaker";
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = useCallback((speakerIdClicked) => {
var speakersArrayUpdated = speakers.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite }; // <= return a copy of rec
}
return rec;
});
setSpeakers(speakersArrayUpdated);
}, []); // <= remove speakers dependency
return (
<div>
{speakers.map((rec) => {
return (
<Speaker
speaker={rec}
key={rec.id}
clickFunction={clickFunction}
></Speaker>
);
})}
</div>
);
};

Resources