Problem with re-renders in React with useCallback and useMemo - reactjs

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>
);
};

Related

Testing zustand state changes caused by a component in Jest

I am pretty new to using jest and Im trying to test a component that makes a state change which acts upon my global state (using Zustand). Basically im clicking a button and its adding an item to my state.traits. Here is my component code:
import { Flex, useToast } from '#chakra-ui/react'
import { FC } from 'react'
import { useProfileStore } from 'stores/profileStore'
interface DataTrait {
name: string,
id: string
}
type Props = {
trait: DataTrait
}
export const ChipItem: FC<Props> = ({ trait }) => {
const { traits, setTraits } = useProfileStore()
const toast = useToast()
const traitNames = traits.map((trait) => trait.name)
const emptyTraits = traits.filter((trait) => trait.name === "")
const handleClick = (trait: DataTrait) => {
if (!traitNames.includes(trait.name) && emptyTraits.length !== 0) {
let currentItem = traits.filter(trait => trait.name === "")[0]
let items = [...traits]
let item = {position: currentItem.position, id: trait.id, name: trait.name}
items[items.indexOf(currentItem)] = item
setTraits(items)
} else if (emptyTraits.length === 0){
toast({
title: 'Error',
status: 'error',
description: 'Only 5 traits can be selected',
isClosable: true,
duration: 5000
})
} else {
toast({
title: 'Error',
status: 'error',
description: 'Please select unique traits',
isClosable: true,
duration: 5000
})
}
}
return (
traitNames.includes(trait.name) ? (
<Flex mx={4} p={2} cursor="pointer" borderRadius="20px" backgroundColor="green" borderWidth="1px" borderColor="white" textColor="white" onClick={() => handleClick(trait)}>{trait.name}</Flex>
) : (
<Flex mx={4} p={2} cursor="pointer" borderRadius="20px" borderWidth="1px" borderColor="grey" onClick={() => handleClick(trait)}>{trait.name}</Flex>
)
)
}
here is my store code:
import create from 'zustand'
export interface Trait {
position: string,
name: string,
id: string,
}
export type Traits = Trait[]
const initialTraits = [
{position: "0", name: "", id: ""},
{position: "1", name: "", id: ""},
{position: "2", name: "", id: ""},
{position: "3", name: "", id: ""},
{position: "4", name: "", id: ""},
]
export type ProfileStore = {
traits: Traits;
setTraits: (traits: Traits) => void;
clearTraits: () => void;
}
export const useProfileStore = create<ProfileStore>((set) => ({
traits: initialTraits,
setTraits: (traits) => set({ traits }),
clearTraits: () => set({ traits: initialTraits })
}))
and here is my test code:
import React from 'react';
import { ChipItem } from "../../ChipList/ChipItem";
import { act, render, renderHook } from "#testing-library/react";
import { useProfileStore } from "../../../stores/profileStore";
const stubbedTrait = {
name: "Doing Work",
id: "efepofkwpeok"
}
it("displays the trait chip", () => {
const { queryByText } = render(<ChipItem trait={stubbedTrait} />);
expect(queryByText("Doing Work")).toBeTruthy();
})
it("sets the chip information in the store", () => {
act(() => {
const { traits } = renderHook(() => useProfileStore())
const { getByText } = render(<ChipItem trait={stubbedTrait}/>);
getByText(stubbedTrait.name).click()
expect(traits.includes(stubbedTrait)).toBeTruthy()
})
})
whats happening, is that it keeps telling me that renderHook is not a function and traits always comes back undefined. any help would be greatly appreciated!
Currently you must install and import React Testing Hooks separately
The best way to unit test Zustand state changes inside and specific component is not by using Zustand but by mocking the store hook with Jest.
You should create a test case for the Zustand Store using React Hook Testing library and once you verify the hook behaves as expected, then you mock the store with manual traits and setTraits changes.
Once you have the unit tests then you should test the behaviour of the real hook and components together with integration tests.

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.

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" />

GSAP animate elements in an array fetched from the server

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.

Resources