Does the order of useCallback function declaration matters? For example:
Functions declared in this order:
const aboveFunction = useCallback(()=>{ logCount() },[logCount])
const logCount = useCallback(()=>{ console.log(count) },[count])
const belowFunction = useCallback(()=>{ logCount() },[logCount])
Notice both aboveFunction and belowFunction refers to the same logCount.
After we call setCount(2) ,
aboveFunction() -> logCount() -> count // 0, wheres
belowFunction() -> logCount() -> count // 2
https://codesandbox.io/s/react-hooks-counter-demo-forked-7mofy?file=/src/index.js
/**
Click increment button until count is 2, then click
Above and Below button and check console log
* Even though the count value is > 0, aboveIncrement console.log(count) still gives 0
*/
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [count, setCount] = useState(0);
/** Assuming all the functions below do some intensive stuffs to justify useCallback */
const aboveFunction = useCallback(() => {
// ...do other intensive stuff
logCount("Above count:"); // Above count: 0
}, [logCount]);
const logCount = useCallback(
(str) => {
// ...do other intensive stuff
console.log(str, count);
},
[count]
);
const belowFunction = useCallback(() => {
// ...do other intensive stuff
logCount("Below count:"); // Above count: 2
}, [logCount]);
return (
<div className="App">
<h1>
Click increment button until count is 2, then click Above/ Below button
and see console log
</h1>
<h2>You clicked {count} times!</h2>
<div
style={{
display: "flex",
alignItems: "center",
flexDirection: "column"
}}
>
<button style={{ margin: 10 }} onClick={aboveFunction}>
Above Increment Function
</button>
<button style={{ margin: 10 }} onClick={() => setCount(count + 1)}>
Increment
</button>
<button style={{ margin: 10 }} onClick={belowFunction}>
Below Increment Function
</button>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
It is apparent that the order of useCallback declaration is important in order for the code to work. However, if we write it this way
https://codesandbox.io/s/react-hooks-counter-demo-forked-7mofy?file=/src/index1.js
let logCount;
const aboveFunction = useCallback(() => {
logCount("Above count:"); // Above count: 0
}, [logCount]);
logCount = useCallback((str) => {
console.log(str, count);
}, [count]
);
const belowFunction = useCallback(() => {
logCount("Below count:"); // Above count: 2
}, [logCount]);
aboveFunction still gives stale count value. Is there any rule-of-hooks that I'm missing?
Yes a fix here is to apply eslint no-use-before-define. But I wanna understand the reason why .. isn’t functions get hoisted up in JavaScript and the order in which it is declared doesn’t matter?
Is there any rule-of-hooks that I'm missing?
You are not missing rule-of-hooks but, you are missing no-use-before-define rule.
You have to use the logCount function above the aboveFunction.
In this attached image as well
you can see, it's showing a warning for function declaration.
Maybe you can add es-lint in your project and make sure you add this plugin & no-use-before-define as well so that it will show you these types of warning in vs code.
Hope this helps!
Related
I'm trying to replicate this stepper like functionality using react.
https://www.commbank.com.au/retail/complaints-compliments-form?ei=CTA-MakeComplaint
Below is my stackblitz, How can I achieve this functionality without using any 3rd Party plugins.
https://stackblitz.com/edit/react-ts-9cfjs3
I set up the basics of the UI on codesandbox.
The main part of how this works is via scrollIntoView using a reference to the div element on each Step. It's important to note that this will work on every modern browser but safari for the smooth scrolling.
Obviously for the actual form parts and moving data around, all of that will still need to be implemented, but this demonstrates nearly all of the navigation/scrolling behaviors as your example.
For reference, here's the main code:
import { useEffect, useRef, useState } from "react";
import A from "./A";
import B from "./B";
import C from "./C";
import D from "./D";
import "./styles.css";
const steps = [A, B, C, D];
export default function App() {
const [step, setStep] = useState(0);
/** Set up a ref that refers to an array, this will be used to hold
* a reference to each step
*/
const refs = useRef<(HTMLDivElement | null)[]>([]);
/** Whenever the step changes, scroll it into view! useEffect needed to wait
* until the new component is rendered so that the ref will properly exist
*/
useEffect(() => {
refs.current[step]?.scrollIntoView({ behavior: "smooth" });
}, [step]);
return (
<div className="App">
{steps
.filter((_, index) => index <= step)
.map((Step, index) => (
<Step
key={index}
/** using `domRef` here to avoid having to set up forwardRef.
* Same behavior regardless, but with less hassle as it's an
* ordianry prop.
*/
domRef={(ref) => (refs.current[index] = ref)}
/** both prev/next handlers for scrolling into view */
toPrev={() => {
refs.current[index - 1]?.scrollIntoView({ behavior: "smooth" });
}}
toNext={() => {
if (step === index + 1) {
refs.current[index + 1]?.scrollIntoView({ behavior: "smooth" });
}
/** This mimics behavior in the reference. Clicking next sets the next step
*/
setStep(index + 1);
}}
/** an override to enable reseting the steps as needed in other ways.
* I.e. changing the initial radio resets to the 0th step
*/
setStep={setStep}
step={index}
/>
))}
</div>
);
}
And component A
import React, { useEffect, useState } from "react";
import { Step } from "./utils";
interface AProps extends Step {}
function A(props: AProps) {
const [value, setValue] = useState("");
const values = [
{ label: "Complaint", value: "complaint" },
{ label: "Compliment", value: "compliment" }
];
const { step, setStep } = props;
useEffect(() => {
setStep(step);
}, [setStep, step, value]);
return (
<div className="step" ref={props.domRef}>
<h1>Component A</h1>
<div>
{values.map((option) => (
<label key={option.value}>
{option.label}
<input
onChange={(ev) => setValue(ev.target.value)}
type="radio"
name="type"
value={option.value}
/>
</label>
))}
</div>
<button
className="next"
onClick={() => {
if (value) {
props.toNext();
}
}}
>
NEXT
</button>
</div>
);
}
export default A;
I'm playing around with a hook that can store some deleted values. No matter what I've tried, I can't get the state from this hook to update when I use it in a component.
const useDeleteRecords = () => {
const [deletedRecords, setDeletedRecords] = React.useState<
Record[]
>([]);
const [deletedRecordIds, setDeletedRecordIds] = React.useState<string[]>([]);
// ^ this second state is largely useless – I could just use `.filter()`
// but I was experimenting to see if I could get either to work.
React.useEffect(() => {
console.log('records changed', deletedRecords);
// this works correctly, the deletedRecords array has a new item
// in it each time the button is clicked
setDeletedRecordIds(deletedRecords.map((record) => record.id));
}, [deletedRecords]);
const deleteRecord = (record: Record) => {
console.log(`should delete record ${record.id}`);
// This works correctly - firing every time the button is clicked
setDeletedRecords(prev => [...prev, record]);
};
const wasDeleted = (record: Record) => {
// This never works – deletedRecordIds is always [] when I call this outside the hook
return deletedRecordIds.some((r) => r === record.id);
};
return {
deletedRecordIds,
deleteRecord,
wasDeleted,
} // as const <-- no change
}
Using it in a component:
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecordIds, wasDeleted, deleteRecord } = useDeleteRecords();
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
}
React.useEffect(() => {
console.log('should fire when deletedRecordIds changes', deletedRecordIds);
// Only fires once for each row on load? deletedRecordIds never changes
// I can rip out the Ids state and do it just with deletedRecords, and the same thing happens
}, [deletedRecordIds]);
}
If it helps, these are in the same file – I'm not sure if there's some magic to exporting a hook in a dedicated module? I also tried as const in the return of the hook but no change.
Here's an MCVE of what's going on: https://codesandbox.io/s/tender-glade-px631y?file=/src/App.tsx
Here's also the simpler version of the problem where I only have one state variable. The deletedRecords state never mutates when I use the hook in the parent component: https://codesandbox.io/s/magical-newton-wnhxrw?file=/src/App.tsx
problem
In your App (code sandbox) you call useDeleteRecords, then for each record you create a DisplayRecord component. So far so good.
function App() {
const { wasDeleted } = useDeleteRecords(); // ✅
console.log("wtf");
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord record={record} /> // ✅
</div>
) : null;
})}
</div>
);
}
Then for each DisplayRecord you call useDeleteRecords. This maintains a separate state array for each component ⚠️
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecords, deleteRecord } = useDeleteRecords(); // ⚠️
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
};
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
// Only fires once for each row on load? deletedRecords never changes
}, [deletedRecords]);
return (
<div>
<div>{record.id}</div>
<div onClick={handleDelete} style={{ cursor: "pointer" }}>
[Del]
</div>
</div>
);
};
solution
The solution is to maintain a single source of truth, keeping handleDelete and deletedRecords in the shared common ancestor, App. These can be passed down as props to the dependent components.
function App() {
const { deletedRecords, deleteRecord, wasDeleted } = useDeleteRecords(); // 👍🏽
const handleDelete = (record) => (event) { // 👍🏽 delete handler
deleteRecord(record);
};
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord
record={record}
deletedRecords={deletedRecords} // 👍🏽 pass prop
handleDelete={handleDelete} // 👍🏽 pass prop
/>
</div>
) : null;
})}
</div>
);
}
Now DisplayRecord can read state from its parent. It does not have local state and does not need to call useDeleteRecords on its own.
const DisplayRecord = ({ record, deletedRecords, handleDelete }) => {
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
}, [deletedRecords]); // ✅ passed from parent
return (
<div>
<div>{record.id}</div>
<div
onClick={handleDelete(record)} // ✅ passed from parent
style={{ cursor: "pointer" }}
children="[Del]"
/>
</div>
);
};
code demo
I would suggest a name like useList or useSet instead of useDeleteRecord. It's more generic, offers the same functionality, but is reusable in more places.
Here's a minimal, verifiable example. I named the delete function del because delete is a reserved word. Run the code below and click the ❌ to delete some items.
function App({ items = [] }) {
const [deleted, del, wasDeleted] = useSet([])
React.useEffect(_ => {
console.log("an item was deleted", deleted)
}, [deleted])
return <div>
{items.map((item, key) =>
<div className="item" key={key} data-deleted={wasDeleted(item)}>
{item} <button onClick={_ => del(item)} children="❌" />
</div>
)}
</div>
}
function useSet(iterable = []) {
const [state, setState] = React.useState(new Set(...iterable))
return [
Array.from(state), // members
newItem => setState(s => (new Set(s)).add(newItem)), // addMember
item => state.has(item) // isMember
]
}
ReactDOM.render(
<App items={["apple", "orange", "pear", "banana"]}/>,
document.querySelector("#app")
)
div.item { display: inline-block; border: 1px solid dodgerblue; padding: 0.25rem; margin: 0.25rem; }
[data-deleted="true"] { opacity: 0.3; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Since you are updating deletedRecordIds inside a React.useEffect, this variable will have the correct value only after the render complete. wasDeleted is a closure that capture the value of deletedRecordIds when the component renders, thus it always have a stale value. As yourself are suggesting, the correct way to do that is to use .filter() and remove the second state.
Talking about the example you provided in both cases you are defining 5 hooks: one hook for each DisplayRecord component and one for the App. Each hook define is own states, thus there are 5 deletedRecords arrays on the page. Clicking on Del, only the array inside that specific component will be updated. All other component won't be notified by the update, because the state change is internal to that specific row. The hook state in App will never change because no one is calling its own deleteRecord function.
You could solve that problem in 2 way:
Pulling up the state: The hook is called just once in the App component and the deleteRecord method is passed as parameter to every DisplayRecord component. I updated your CodeSandbox example.
Use a context: Context allows many component to share the same state.
Why does a whole component in react re-render when you change state in a onClick?
exmaple : https://codesandbox.io/s/vibrant-firefly-sgk5g?file=/src/App.js
When you click on the numbers the whole components re-renders , and if you remove the setCount from the on click function it works just fine
The idea behind the component is to add a "Active" class to the number that you have clicked, and it updated a random counter, that counter prevents the addition the "active" class, since it re-renders the whole component
EDIT: code here aswell
import React, { useState } from "react";
const Hours = () => {
const days = [1, 2, 3, 4, 5, 6];
const [count, setCount] = useState(1);
const TestClick = (e, item) => {
setCount(count + 1);
e.currentTarget.className = "active";
};
const HandleHours = () => {
let block = <span />;
if (days) {
block = days.map((hour, index) => {
return (
<span
style={{ display: "block" }}
onClick={e => {
TestClick(e, hour);
}}
className={`col-md-4`} key={index}>
{hour}
</span>
);
});
}
return block;
};
return (
<div>
<HandleHours />
</div>
);
};
export default Hours;
The issue here isn't coming from the fact that the HandleHours components render but because it gets remounted everytime you change the state in the Hours component.
This happens because HandleHours is defined as a component within Hours component and everytime Hours re-renders a new reference to HandleHours is created which fools react into thinking that the component detached from DOM and a new component replaces it, since it essentialy works on reference.
Now when you render HandleHours like
<div>
{ HandleHours () }
</div>
Suddenly HandleHours turns from being a component to a function which returns JSX so this time when the Hours component re-renders, even though the function reference to HandleHours has changed. It returns the JSX with a key prop on it, which remains the same and hence React treats it as a re-render and hour changes to DOM elements aren't lost
Now there is a solution to the first approach too
All you need to do is to create a component HandleHours outside of your Hours component and render it by passing the required props like
import React, { useState } from "react";
import "./styles.css";
const HandleHours = ({ days, TestClick }) => {
let block = <span />;
if (days) {
block = days.map((hour, index) => {
return (
<span
style={{ display: "block" }}
onClick={e => {
TestClick(e, hour);
}}
className={`col-md-4`}
key={index}
>
{hour}
</span>
);
});
}
return block;
};
const days = [1, 2, 3, 4, 5, 6];
const Hours = () => {
const [count, setCount] = useState(1);
const TestClick = (e, item) => {
setCount(count + 1);
console.log("TestClick");
e.currentTarget.className = "active";
};
return (
<div>
<HandleHours days={days} TestClick={TestClick} />
</div>
);
};
export default Hours;
When you do that the HandleHours component isn't re-mounted on each rerender of Hours component and it maintains the DOM elements correctly.
Here is a working demo for the second approach
It's the way react rerenders when a component state changes. The state hook rerenders the whole component that it's in when the setState function is called which is the second element in the array that useState returns.
If you want to change the class of an element on click, you need to store it as a state. In your code, the class of clicked span is updated on click, but right after that the component is rerendered and set to what the HandleHours returns.
I would probalby have a state that keeps track which day is clicked and render that accordingly (not sure why you need the count, but I left it there):
import React, { useState } from "react";
const Hours = () => {
const days = [1, 2, 3, 4, 5, 6];
const [count, setCount] = useState(1);
const [clickedDays, setClickedDays] = useState([]); // Added clickedDays state
const TestClick = (e, item, isDayClicked) => {
setCount(count + 1);
if (!isDayClicked) { // Setting clicked days if they are not in the array yet
setClickedDays([...clickedDays, item])
}
};
const HandleHours = () => {
let block = <span />;
if (days) {
block = days.map((hour, index) => {
const isDayClicked = clickedDays.includes(hour);
return (
<span
style={{ display: "block" }}
onClick={e => {
TestClick(e, hour, isDayClicked);
}}
className={isDayClicked ? 'active' : 'col-md-4'} // Setting different class depending on state
key={index}
>
{hour}
</span>
);
});
}
return block;
};
return (
<div>
<HandleHours />
</div>
);
};
export default Hours;
I tried chaining two springs (using useChain), so that one only starts after the other finishes, but they are being animated at the same time. What am I doing wrong?
import React, { useRef, useState } from 'react'
import { render } from 'react-dom'
import { useSpring, animated, useChain } from 'react-spring'
function App() {
const [counter, setCounter] = useState(0)
const topRef = useRef()
const leftRef = useRef()
const { top } = useSpring({ top: (window.innerHeight * counter) / 10, ref: topRef })
const { left } = useSpring({ left: (window.innerWidth * counter) / 10, ref: leftRef })
useChain([topRef, leftRef])
return (
<div id="main" onClick={() => setCounter((counter + 1) % 10)}>
Click me!
<animated.div id="movingDiv" style={{ top, left }} />
</div>
)
}
render(<App />, document.getElementById('root'))
Here's a codesandbox demonstrating the problem:
https://codesandbox.io/s/react-spring-usespring-hook-m4w4t
I just found out that there's a much simpler solution, using only useSpring:
function App() {
const [counter, setCounter] = useState(0)
const style = useSpring({
to: [
{ top: (window.innerHeight * counter) / 5 },
{ left: (window.innerWidth * counter) / 5 }
]
})
return (
<div id="main" onClick={() => setCounter((counter + 1) % 5)}>
Click me!
<animated.div id="movingDiv" style={style} />
</div>
)
}
Example: https://codesandbox.io/s/react-spring-chained-animations-8ibpi
I did some digging as this was puzzling me as well and came across this spectrum chat.
I'm not sure I totally understand what is going on but it seems the current value of the refs in your code is only read once, and so when the component mounts, the chain is completed instantly and never reset. Your code does work if you put in hardcoded values for the two springs and then control them with turnaries but obviously you are looking for a dynamic solution.
I've tested this and it seems to do the job:
const topCurrent = !topRef.current ? topRef : {current: topRef.current};
const leftCurrent = !leftRef.current ? leftRef : {current: leftRef.current};
useChain([topCurrent, leftCurrent]);
It forces the chain to reference the current value of the ref each time. The turnary is in there because the value of the ref on mount is undefined - there may be a more elegant way to account for this.
I am playing around with React Hooks. When the button is clicked, I want to increment a counter. After the counter was incremented, the application should not allow further increments until clicked is reset to false.
I came up with this:
function App() {
const [counter, setCounter] = useState(0);
const [clicked, setClicked] = useState(false);
useEffect(() => {
if (clicked) {
setCounter(counter + 1);
setTimeout(() => {
setClicked(false);
}, 2000);
}
}, [clicked]);
return (
<div className="App">
<p>Clicked: {String(clicked)}</p>
<p>Counter: {counter}</p>
<button type="button" onClick={() => setClicked(true)}>
Click me
</button>
</div>
);
}
It actually works. However React is complaining with following warning:
React Hook useEffect has a missing dependency: 'counter'. Either
include it or remove the dependency array. You can also do a
functional update 'setCounter(c => ...)' if you only need 'counter' in
the 'setCounter' call. (react-hooks/exhaustive-deps)
When I add the counter to the dependencies, useEffect will get into an infinite loop, because clicked is true and setCounter was called from within useEffect.
I want the counter only to be incremented, when clicked changed from false to true. That works if the dependency list only contains clicked, but React complains about that.
Try out for yourself: https://codesandbox.io/s/dreamy-shadow-7xesm
Try replacing setCounter(counter + 1) with this:
setCounter(counter => counter + 1)
Like the warning says. Should solve it.
Your problem with infinite loop will be gone if you remove the timeout. (btw what is it for? Are you trying to implement a debounce or throttle?)
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [counter, setCounter] = useState(0);
const [clicked, setClicked] = useState(false);
useEffect(() => {
if (clicked) {
setClicked(false);
setCounter(counter + 1);
}
}, [clicked, counter]);
return (
<div className="App">
<p>Clicked: {String(clicked)}</p>
<p>Counter: {counter}</p>
<button type="button" onClick={() => setClicked(true)}>
Click me
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);