Equivalent of setState callback with React hooks - javascript

I have a modal with a list of answers.
I can either click an answer to select it, then click a button to confirm my choice.
Or I can double-click an answer to select it and confirm.
I'm having trouble properly handling the double-click case.
With React class components, I would have used setState()'s callback like this:
setState({selectedAnswer: answer}, confirm)
But right now, I only figured out the following:
const MyModal = ({hide, setAnwser}) => {
const [selectedAnswer, setSelectedAnswer] = useState(null);
const [isSelectionDone, setIsSelectionDone] = useState(false);
const confirm = () => {
if (!selectedAnswer) {
return;
}
setAnwser(selectedAnswer);
hide();
};
const handleAnswerOnClick = (answer) => {
setSelectedAnswer(answer);
};
const handleAnswerOnDoubleClick = (answer) => {
setSelectedAnswer(answer);
setIsSelectionDone(true);
};
useEffect(confirm, [isSelectionDone]);
return (
<div>
<div>{answers.map((answer) => <MyAnswer
isSelected={answer.id === selectedAnswer?.id}
key={answer.id}
answer={answer}
onClick={handleAnswerOnClick}
onDoubleClick={handleAnswerOnDoubleClick}/>)}</div>
<button onClick={confirm}>Confirm</button>
</div>
);
}
I strongly suspect that there's a nicer/better way of doing it.
Maybe a simple:
const MyModal = ({hide, setAnwser}) => {
const [selectedAnswer, setSelectedAnswer] = useState(null);
const confirm = () => {
if (!selectedAnswer) {
return;
}
setAnwser(selectedAnswer);
hide();
};
const handleAnswerOnClick = (answer) => {
setSelectedAnswer(answer);
};
const handleAnswerOnDoubleClick = (answer) => {
setAnwser(answer);
hide();
};
return (
<div>
<div>{answers.map((answer) => <MyAnswer
isSelected={answer.id === selectedAnswer?.id}
key={answer.id}
answer={answer}
onClick={handleAnswerOnClick}
onDoubleClick={handleAnswerOnDoubleClick}/>)}</div>
<button onClick={confirm}>Confirm</button>
</div>
);
}
Which way is better?

There is no similar set state in hooks (which fires callback after state is set).
But, you could apply following refactor:
const confirm = (sAnswer = selectedAnswer) => {
if (!sAnswer) {
return;
}
setAnwser(sAnswer);
hide();
};
And then
const handleAnswerOnDoubleClick = (answer) => {
setSelectedAnswer(answer);
confirm(answer);
};

Related

React recoil, overlay with callback and try finally

I want to render overlay on the long running operations.
Consider I have the following code
let spinnerState = useRecoilValue(overlayState);
return <BrowserRouter>
<Spin indicator={<LoadingOutlined />} spinning={spinnerState.shown} tip={spinnerState.content}>.........</BrowserRouter>
What I do in different components
const [, setOverlayState] = useRecoilState(overlayState);
const onButtonWithLongRunningOpClick = async () => {
Modal.destroyAll();
setOverlayState({
shown: true,
content: text
});
try {
await myApi.post({something});
} finally {
setOverlayState(overlayStateDefault);
}
}
How can I refactor this to use such construction that I have in this onbuttonclick callback? I tried to move it to the separate function, but you cannot use hooks outside of react component. It's frustrating for me to write these try ... finally every time. What I basically want is something like
await withOverlay(async () => await myApi.post({something}), 'Text to show during overlay');
Solution
Write a custom hook that includes both UI and API. This pattern is widely used in a large app but I couldn't find the name yet.
// lib/buttonUi.js
const useOverlay = () => {
const [loading, setLoading] = useState(false);
return {loading, setLoading, spinnerShow: loading };
}
export const useButton = () => {
const overlay = useOverlay();
const someOp = async () => {
overlay.setLoading(true);
await doSomeOp();
/* ... */
overlay.setLoading(false);
}
return {someOp, ...overlay}
}
// components/ButtonComponent.jsx
import { useButton } from 'lib/buttonUi';
const ButtonComponent = () => {
const {spinnerShow, someOp} = useButton();
return <button onClick={someOp}>
<Spinner show={spinnerShow} />
</button>
}
export default ButtonComponent;
create a custom hook that handles the logic for showing and hiding the overlay.
import { useRecoilState } from 'recoil';
const useOverlay = () => {
const [, setOverlayState] = useRecoilState(overlayState);
const withOverlay = async (fn: () => Promise<void>, content: string) => {
setOverlayState({ shown: true, content });
try {
await fn();
} finally {
setOverlayState(overlayStateDefault);
}
};
return withOverlay;
};
You can then use the useOverlay hook in your components
import { useOverlay } from './useOverlay';
const Component = () => {
const withOverlay = useOverlay();
const onButtonWithLongRunningOpClick = async () => {
await withOverlay(async () => await myApi.post({ something }), 'Text to show during overlay');
};
return <button onClick={onButtonWithLongRunningOpClick}>Click me</button>;
};

State not updating until I add or remove a console log

const BankSearch = ({ banks, searchCategory, setFilteredBanks }) => {
const [searchString, setSearchString] = useState();
const searchBanks = (search) => {
const filteredBanks = [];
banks.forEach((bank) => {
if (bank[searchCategory].toLowerCase().includes(search.toLowerCase())) {
console.log(bank[searchCategory].toLowerCase());
filteredBanks.push(bank);
}
});
setFilteredBanks(filteredBanks);
};
const debounceSearch = useCallback(_debounce(searchBanks, 500), []);
useEffect(() => {
if (searchString?.length) {
debounceSearch(searchString);
} else setFilteredBanks([]);
}, [searchString, searchCategory]);
const handleSearch = (e) => {
setSearchString(e.target.value);
};
return (
<div className='flex'>
<Input placeholder='Bank Search' onChange={handleSearch} />
</div>
);
};
export default BankSearch;
filteredBanks state is not updating
banks is a grandparent state which has a lot of objects, similar to that is filteredBanks whose set method is being called here which is setFilteredBanks
if I add a console log and save or remove it the state updates
Adding or removing the console statement and saving the file, renders the function again, the internal function's state is updated returned with the (setState) callback.
(#vnm)
Adding filteredBanks to your dependency array won't do much because it is part of the lexical scope of the function searchBanks
I'm not entirely sure of the total context of this BankSearch or what it should be. What I do see is that there are some antipatterns and missing dependencies.
Try this:
export default function BankSearch({ banks, searchCategory, setFilteredBanks }) {
const [searchString, setSearchString] = useState();
const searchBanks = useCallback(
search => {
const filteredBanks = [];
banks.forEach(bank => {
if (bank[searchCategory].toLowerCase().includes(search.toLowerCase())) {
filteredBanks.push(bank);
}
});
setFilteredBanks(filteredBanks);
},
[banks, searchCategory, setFilteredBanks]
);
const debounceSearch = useCallback(() => _debounce(searchBanks, 500), [searchBanks]);
useEffect(() => {
if (searchString?.length) {
debounceSearch(searchString);
} else setFilteredBanks([]);
}, [searchString, searchCategory, setFilteredBanks, debounceSearch]);
const handleSearch = e => {
setSearchString(e.target.value);
};
return (
<div className="flex">
<Input placeholder="Bank Search" onChange={handleSearch} />
</div>
)}
It feels like the component should be a faily simple search and filter and it seems overly complicated for what it needs to do.
Again, I don't know the full context, however, I'd look into the compont architecture/structuring of the app and state.

Generator function inside useCallback is returning same values in react, How to solve this?

I am creating to-do app in react and for the id of task i am using generator function. But This generator function is giving value 0 everytime and not incrementing the value.I think the reason for issue is useCallback() hook but i am not sure what can be the solution.How to solve the issue?Here i am providing the code :
import DateAndDay, { date } from "../DateAndDay/DateAndDay";
import TaskList, { TaskProps } from "../TaskList/TaskList";
import "./ToDo.css";
import Input from "../Input/Input";
import { ChangeEvent, useCallback, useEffect, useState } from "react";
function ToDo() {
const [inputShow, setInputShow] = useState(false);
const [valid, setValid] = useState(false);
const [enteredTask, setEnteredTask] = useState("");
const [touched, setTouched] = useState(false);
const [tasks, setTasks] = useState<TaskProps[]>(() => {
let list = localStorage.getItem("tasks");
let newdate = String(date);
const setdate = localStorage.getItem("setdate");
if (newdate !== setdate) {
localStorage.removeItem("tasks");
}
if (list) {
return JSON.parse(list);
} else {
return [];
}
});
const activeHandler = (id: number) => {
const index = tasks.findIndex((task) => task.id === id);
const updatedTasks = [...tasks];
updatedTasks[index].complete = !updatedTasks[index].complete;
setTasks(updatedTasks);
};
const clickHandler = () => {
setInputShow((prev) => !prev);
};
const input = inputShow && (
<Input
checkValidity={checkValidity}
enteredTask={enteredTask}
valid={valid}
touched={touched}
/>
);
const btn = !inputShow && (
<button className="add-btn" onClick={clickHandler}>
+
</button>
);
function checkValidity(e: ChangeEvent<HTMLInputElement>) {
setEnteredTask(e.target.value);
}
function* idGenerator() {
let i = 0;
while (true) {
yield i++;
}
}
let id = idGenerator();
const submitHandler = useCallback(
(event: KeyboardEvent) => {
event.preventDefault();
setTouched(true);
if (enteredTask === "") {
setValid(false);
} else {
setValid(true);
const newtitle = enteredTask;
const newComplete = false;
const obj = {
id: Number(id.next().value),
title: newtitle,
complete: newComplete,
};
setTasks([...tasks, obj]);
localStorage.setItem("setdate", date.toString());
setEnteredTask("");
}
},
[enteredTask, tasks, id]
);
useEffect(() => {
const handleKey = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setInputShow(false);
}
if (event.key === "Enter") {
submitHandler(event);
}
};
document.addEventListener("keydown", handleKey);
return () => {
document.removeEventListener("keydown", handleKey);
};
}, [submitHandler]);
useEffect(() => {
localStorage.setItem("tasks", JSON.stringify(tasks));
}, [tasks]);
return (
<div className="to-do">
<DateAndDay />
<TaskList tasks={tasks} activeHandler={activeHandler} />
{input}
{btn}
</div>
);
}
export default ToDo;
useCallBack()'s is used to memorize the result of function sent to it. This result will never change until any variable/function of dependency array changes it's value. So, please check if the dependencies passed are correct or if they are changing in your code or not ( or provide all the code of this file). One of my guess is to add the Valid state as dependency to the array
It's because you are calling the idGenerator outside of the useCallback, so it is only generated if the Component is re-rendered, in your case... only once.
Transfer it inside useCallback and call it everytime the event is triggered:
// wrap this on a useCallback so it gets memoized
const idGenerator = useCallback(() => {
let i = 0;
while (true) {
yield i++;
}
}, []);
const submitHandler = useCallback(
(event: KeyboardEvent) => {
event.preventDefault();
let id = idGenerator();
// ... rest of logic
},
[enteredTask, tasks, idGenerator]
);
If you're using the generated id outside the event handler, store the id inside a state like so:
const idGenerator = useCallback(() => {
let i = 0;
while (true) {
yield i++;
}
}, []);
const [id, setId] = useState(idGenerator());
const submitHandler = useCallback(
(event: KeyboardEvent) => {
event.preventDefault();
let newId = idGenerator();
setId(newId)
// ... rest of logic
},
[enteredTask, tasks, id, idGenerator]
);

How to use useeffect hook in react?

i want to return a function that uses useEffect from the usehook and i am getting error "useeffect is called in a function which is neither a react function component or custom hook.
what i am trying to do?
i have addbutton component and when user clicks add button i want to call the function requestDialog.
below is my code within addbutton file
function AddButton () {
const count = useGetCount();
const requestDialog = useRequestDialog(); //using usehook here
const on_add_click = () => {
requestDialog(count); //calling requestDialog here
}
return (
<button onClick={on_add_click}>add</button>
);
}
interface ContextProps {
trigger: (count: number) => void;
}
const popupContext = React.createContext<ContextProps>({
trigger: (availableSiteShares: number) => {},
});
const usePopupContext = () => React.useContext(popupContext);
export const popupContextProvider = ({ children }: any) => {
const [show, setShow] = React.useState(false);
const limit = 0;
const dismiss = () => {
if (show) {
sessionStorage.setItem(somePopupId, 'dismissed');
setShow(false);
}
};
const isDismissed = (dialogId: string) =>
sessionStorage.getItem(dialogId) === 'dismissed';
const context = {
trigger: (count: number) => {
if (!isDismissed(somePopupId) && count <= limit) {
setShow(true);
} else if (count > limit) {
setShow(false);
}
},
};
return (
<popupContext.Provider value={context}>
{children}
{show && (
<Popup onHide={dismiss} />
)}
</popupContext.Provider>
);
};
export function useRequestDialog(enabled: boolean,count: number) {
return function requestDialog() { //here is the error
const { trigger } = usePopupContext();
React.useEffect(() => {
trigger(count);
}
}, [count, trigger]);
}
How to solve the error ""useEffect is called in a function which is neither a react function component or custom hook."
i am not knowing how to use useeffect and the same time use it in the addbutton component.
could someone help me with this. thanks
useEffect method is like, useEffect(() => {}, []), But your usage in requestDialog is wrong. Try changing with following.
function requestDialog() {
const { trigger } = usePopupContext();
React.useEffect(() => {
trigger(count);
}, [count, trigger]);
}

How to test window.open is being called from a react component

I have a react component that renders a series of other components, each with their own checkbox.
There is a state hook called rulesToDownload which begins as an empty array and has ids added to / removed from it as checkboxes are checked / unchecked.
When the 'download' button is clicked, the rulesToDownload array is passed to a data function DownloadFundDataById that forEach's over the array and calls window.open for each value with an api call with the id appended. The data function is imported into the component, not passed in as a prop.
This causes multiple tabs to flash up before closing when the data downloads. It's not perfect but it works.
I want to complete my test coverage and need to test that the function gets called on button click, and that it does what it should.
Any help appreciated.
Code below:
Summary.test.js:
it(`should create correct download array when some rules are selected`, async () => {
global.open = sandbox.spy();
fetch.mockResponseOnce(JSON.stringify(selectedRules));
wrapper = mount(<Summary/>);
await act(async () => {} );
wrapper.update();
wrapper.find('ReportProgressSummary').first().find('input').last().simulate('change', {target: {checked: true}});
wrapper.find('button').first().simulate('click');
expect(global.open).to.have.been.called();
});
I can confirm that all the 'find' statements are correct, and correctly update the checked value.
Summary.js:
const Summary = () => {
const [expand, setExpand] = useState(false);
const [buttonText, setButtonText] = useState("expand other rules");
const [rulesToDownload, setRulesToDownload] = useState([]);
const [data, setData] = useState([]);
const [dataLoadComplete, setDataLoadComplete] = useState(false);
const [dataLoadFailed, setDataLoadFailed] = useState(false);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
let importedData = await ExecuteRules();
setData(importedData);
setDataLoadComplete(true);
} catch (_) {
setDataLoadFailed(true);
}
};
const onButtonClick = () => {
setExpand(!expand);
if(!expand) setButtonText("hide other rules");
else setButtonText("expand other rules");
};
const modifyDownloadArray = (id, checked) => {
let tempArray;
if(checked) tempArray = [...rulesToDownload, id];
else tempArray = [...rulesToDownload.filter(ruleId => ruleId !== id)];
setRulesToDownload([...tempArray]);
};
const dataFilter = (inputData, isFavouriteValue) => {
return inputData.filter(rule => rule.isFavourite === isFavouriteValue)
.sort((a, b) => a.percentage - b.percentage)
.map((rule, i) => {
return <ReportProgressSummary
result={rule.percentage}
id={rule.id}
title={rule.name} key={i}
modifyDownloadArray={modifyDownloadArray}
/>
})
};
return (
<div className="test">
{
dataLoadFailed &&
<div>Rule load failed</div>
}
{
!dataLoadComplete &&
<LoadingSpinnerTitle holdingTitle="Loading rule data..."/>
}
{
dataLoadComplete &&
<Fragment>
<PageTitle title="System Overview"/>
<LineAndButtonContainerStyled>
<ContainerStyled>
{
dataFilter(data, true)
}
</ContainerStyled>
<ContainerStyled>
<ButtonStyled
disabled={!rulesToDownload.length}
onClick={() => DownloadFundDataById(rulesToDownload)}>
download
</ButtonStyled>
</ContainerStyled>
</LineAndButtonContainerStyled>
<LineBreakStyled/>
<ButtonStyled onClick={() => onButtonClick()}>{buttonText}</ButtonStyled>
{
expand &&
<ContainerStyled>
{
dataFilter(data, false)
}
</ContainerStyled>
}
</Fragment>
}
</div>
)
};
export default Summary;
DataMethod.js:
export function DownloadFundDataById(downloadArray) {
downloadArray.forEach(id => window.open(baseApiUrl + '/xxxx/xxxx/' + id));
}
I can confirm the url is fine, just replaced for now
TestSetup:
const doc = jsdom.jsdom('<!doctype html><html><body></body></html>')
global.document = doc;
global.window = doc.defaultView;
configure({ adapter: new Adapter() });
global.expect = expect;
global.sandbox = sinon.createSandbox();
global.React = React;
global.mount = mount;
global.shallow = shallow;
global.render = render;
global.fetch = jestFetchMock;
global.act = act;
chai.use(chaiAsPromised);
chai.use(sinonChai);
chai.use(chaiEnzyme());
chai.use(chaiJestDiff());
console.error = () => {};
console.warn = () => {};
Current test output says that global.open is not being called. I know this makes sense as it isn't actually assigned as a prop to the onClick of the button or anything. This I think is one of my issues - I can't assign a stub to the button directly, but I'm trying not to re-write my code to fit my tests...
Managed to get this working with a couple of updates to my test file:
it(`should create correct download array when some rules are selected`, async () => {
global.open = sandbox.stub(window, "open");
fetch.mockResponseOnce(JSON.stringify(selectedRules));
wrapper = mount(<Summary/>);
await act(async () => {} );
wrapper.update();
wrapper.find('ReportProgressSummary').first().find('input').last().simulate('change', {target: {checked: true}});
wrapper.find('button').first().simulate('click');
expect(global.open).to.have.been.called;
});
the sandbox.spy() was updated to a sandbox.stub() with (window, "open")
thanks to this article for the help!
https://github.com/mrdulin/mocha-chai-sinon-codelab/blob/master/src/stackoverflow/53524524/index.spec.js
Also the expect statement using to.be.called() is actually not a function and so was updated to to.be.called

Categories

Resources