Is it possible to call useCallback inside another callback - reactjs

in component A:
const getOnClick = useCallback(
(rec: GenericRec): (() => void) => () => {
setSelectedRecord(rec);
},
[],
);
in componant B child of A :
const openRecord = useCallback(
(row: Row<Record>) => () => {
getOnClick({ id: row.original.id, name: row.original.name });
},
[getOnClick],
);
getOnClick is not called

You have an extra layer of functions in both of your useCallback calls. Unlike useMemo, useCallback directly accepts the callback that it should memoize, not a function that builds the callback.
So:
const getOnClick = useCallback(
(rec: GenericRec): (() => void) => {
// ^−−−− no () => here
console.log("getOnClick");
setSelectedRecord(rec);
},
[],
);
const openRecord = useCallback(
(row: Row<Record>) =>
// ^−−−− no () => here
getOnClick({ id: row.original.id, name: row.original.name })
,
[getOnClick],
);
Live Example (with TypeScript type annotations commented out):
const { useState, useCallback } = React;
function Example() {
const [selectedRecord, setSelectedRecord] = useState(null);
const getOnClick = useCallback(
(rec/*: GenericRec*/)/*: (() => void)*/ => {
console.log("getOnClick");
setSelectedRecord(rec);
},
[]/*,*/
);
const openRecord = useCallback(
(row/*: Row<Record>*/) =>
// ^−−−− no () => here
getOnClick({ id: row.original.id, name: row.original.name })
,
[getOnClick]/*,*/
);
const addRecord =() => {
console.log("addRecord");
openRecord({
original: {
id: 1,
name: "The record"
}
});
};
return (
<div>
<input type="button" value="Click Me" onClick={addRecord} disabled={!!selectedRecord} />
{selectedRecord && <span>Selected record: "{selectedRecord.name}" ({selectedRecord.id})</span>}
{!selectedRecord && <em>No record selected</em>}
</div>
);
}
ReactDOM.render(<Example/>, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

Related

React mock asynchronous axios with jest doesn't work

I'm trying to test the component below using mock axios, however, it looks like the components are not rendered as expected, could someone help me on that? I have been stuck for quite a while. The component is fetching an api every 1 second.
const RealtimePrice = () => {
var [cryptoFeed, setCryptoFeed] = useState<cryptoFeed>([]);
var [currency, setCurrency] = useState(currencyList[0]);
var [cryptoSearch, setCryptoSearch] = useState("");
const url = `https://api.coingecko.com/api/v3/coins/markets?ids=${ids}&vs_currency=${currency}`;
const intervalRef = useRef<NodeJS.Timer>();
const onCurrencyChangeHandler = useCallback((newValue: string) => {
setCurrency(newValue);
}, []);
const onCryptoSearchChangeHandler = useCallback((newValue: string) => {
setCryptoSearch(newValue);
}, []);
useEffect(() => {
const getCryptoFeed = () => {
axios.get(url).then((response: any) => {
if (response.data) {
console.debug("The state is set");
setCryptoFeed(response.data);
} else {
console.debug("The state is not set");
setCryptoFeed([]);
}
});
};
getCryptoFeed();
intervalRef.current = setInterval(getCryptoFeed, 1000);
return () => {
clearInterval(intervalRef.current);
};
}, [url]);
const priceBlocks = cryptoFeed
.filter((crypto) =>
crypto.name.toLowerCase().includes(cryptoSearch.toLowerCase())
)
.map((crypto: any) => {
return (
<PriceBlock
key={crypto.id}
id={crypto.id}
name={crypto.name}
price={crypto.current_price}
volume={crypto.total_volume}
change={crypto.price_change_24h}
></PriceBlock>
);
});
return (
<div className={styles.container}>
<div className={styles["header-section"]}>
<h1>Cryptocurrency Realtime Price</h1>
<div className="input-group">
<Selectbox
onChange={onCurrencyChangeHandler}
defaultOption={currencyList[0]}
options={currencyList}
/>
<Inputbox
placeHolder="Enter crypto name"
onChange={onCryptoSearchChangeHandler}
/>
</div>
</div>
<div className={styles.priceblocks}>{priceBlocks}</div>
</div>
);
};
The test is the defined as the following, findByText gives error, it couldn't find the element.
import { render, screen } from "#testing-library/react";
import RealtimePrice from "../RealtimePrice";
describe("Realtime Price", () => {
it("should render the Bitcoin price block", async () => {
render(<RealtimePrice />);
const pb = await screen.findByText("Bitcoin");
expect(pb).toBeInTheDocument();
});
});
And in package.json I have set
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}"
],
"resetMocks": false
}
In src/mocks/axios.js
const mockGetResponse = [
{
id: "bitcoin",
name: "Bitcoin",
price: 20000,
volume: 12004041094,
change: -12241,
},
{
id: "solana",
name: "Solana",
price: 87,
volume: 200876648,
change: 122,
},
];
const mockResponse = {
get: jest.fn().mockResolvedValue(mockGetResponse),
};
export default mockResponse;
With our comments seems clear the issue is that mock is not returning a proper response.data (that's why u are setting an empty array as the state)
Try doing:
const mockResponse = {
get: jest.fn().mockResolvedValue({data: mockGetResponse}),
};

I'm having some trouble trying to change specific button's value when clicked and get back to the previous value with setTimeout()

Here's my example code (I'm trying in a separated component with different Data.
import React, { useState } from 'react';
const initialState = [
{ id: 0, text: 'add to cart' },
{ id: 1, text: 'add to cart' },
{ id: 2, text: 'add to cart' },
];
const Test: React.FC = () => {
const [text, setText] = useState(initialState);
const handleClick = (index: number) => {
const newText = [...text];
newText[index].text = 'Added to cart...';
setText(newText);
setTimeout(() => {
setText(initialState);
}, 2000);
};
return (
<div>
{text.map((buttons, index) => (
<button key={index} onClick={() => handleClick(index)}>
{buttons.text}
</button>
))}
</div>
);
};
export default Test;
You should deep clone your array with JSON.parse(JSON.stringify(...)) since you are editing nested properties.
This code should work:
const handleClick = (index: number) => {
const newText = JSON.parse(JSON.stringify(text));
newText[index].text = 'Added to cart...';
setText(newText);
setTimeout(() => {
setText(initialState);
}, 2000);
};

How to test onClick() funct and useState hooks using jest and enzyme

I am new to this jest+enzyme testing and I am stuck at how to cover the lines and functions such as onClick(), the useState variables and also useffect(). Can anyone with any experience in such scenerios please give me some direction on how to do that efficiently.
Below is the code:
export interface TMProps {
onClick: (bool) => void;
className?: string;
style?: object;
}
export const TM: React.FC<TMProps> = (props) => {
const {onClick} = props;
const [isMenuOpen, toggleMenu] = useState(false);
const handleUserKeyPress = (event) => {
const e = event;
if (
menuRef &&
!(
(e.target.id && e.target.id.includes("tmp")) ||
(e.target.className &&
(e.target.className.includes("tmp-op") ||
e.target.className.includes("tmp-option-wrapper")))
)
) {
toggleMenu(false);
}
};
useEffect(() => {
window.addEventListener("mousedown", handleUserKeyPress);
return () => {
window.removeEventListener("mousedown", handleUserKeyPress);
};
});
return (
<React.Fragment className="tmp">
<Button
className={props.className}
style={props.style}
id={"lifestyle"}
onClick={() => toggleMenu((state) => !state)}>
Homes International
<FontAwesomeIcon iconClassName="fa-caret-down" />{" "}
</Button>
<Popover
style={{zIndex: 1200}}
id={`template-popover`}
isOpen={isMenuOpen}
target={"template"}
toggle={() => toggleMenu((state) => !state)}
placement="bottom-start"
className={"homes-international"}>
<PopoverButton
className={
"template-option-wrapper homes-international"
}
textProps={{className: "template-option"}}
onClick={() => {
onClick(true);
toggleMenu(false);
}}>
Generic Template{" "}
</PopoverButton>
/>
}
Here is the test I have written but it isn't covering the onClick(), useEffect() and handleUserKeyPress() function.
describe("Modal Heading", () => {
React.useState = jest.fn().mockReturnValueOnce(true)
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(Button)).toHaveLength(1);
});
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(Popover)).toHaveLength(1);
});
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(PopoverButton)).toHaveLength(1);
});
What you're looking for is enzyme's:
const btn = wrapper.find('lifestyle');
btn.simulate('click');
wrapper.update();
Not sure if it'd trigger the window listener, it's possible you'll have to mock it.

React hooks- how to set options in dropdown with a variable

I'm working in a project where I'm trying to set the options of a dropdown with an array of objects that I'm getting from a request to an API.
The thing is that when I'm setting the options (key, value, text) with a map of that variable, it appears an error. So I think that way is not correct to what I'm doing.
Can you help me to know what to do in this case?
Here is my code:
import { Dropdown } from 'semantic-ui-react';
type formProps = {
funcionCierre: any
carrera: any;
nombre1: any;
}
const Estudiantes: React.FC<formProps> = (props: formProps) => {
const [area, setArea] = useState<any[]>([]);
useEffect(() => {
console.log(props.carrera);
axios.get('http://localhost:8003/skill?carrera_id=' + props.carrera + '&tipo_id=1')
.then(result => {
console.log(result);
setArea(result.data); //here is where i'm capturing my array of options
console.log(area);
}
).catch(error => {
console.log(error);
});
}, [area.length]);
return (
<Dropdown
placeholder='Area'
search
selection
options={area.map(ar => (
key: ar.skil_id, //here is where i'm trying to set the options
value: ar.skill_id,
text: ar.nombre
))}
/>)
Thanks in advance.
You are missing {} from your area.map(ar => (...)) call. It should be area.map(ar => ({...}))
const Dropdown = (props) => <div>{JSON.stringify(props.options)}</div>;
const Estudiantes = (props) => {
const [area, setArea] = React.useState([]);
React.useEffect(() => {
Promise.resolve([{skill_id: 1, nombre: 'one'}, {skill_id: 2, nombre: 'two'}])
.then(result => setArea(result))
}, [area.length]);
return (
<Dropdown
placeholder='Area'
options={area.map(ar => ({
key: ar.skil_id,
value: ar.skill_id,
text: ar.nombre
}))}
/>
)
}
ReactDOM.render(<Estudiantes />, document.getElementById("app"));
<script src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="app"></div>

Multiple useEffect and setState causing callback to be called twice

I'm test driving a pattern I found online known as meiosis as an alternative to Redux using event streams. The concept is simple, the state is produced as a stream of update functions using the scan method to evaluate the function against the current state and return the new state. It works great in all of my test cases but when I use it with react every action is called twice. You can see the entire app and reproduce the issue at CodeSandbox.
import state$, { actions } from "./meiosis";
const App = () => {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState({
title: "",
status: "PENDING"
});
useEffect(() => {
state$
.pipe(
map(state => {
return state.get("todos")
}),
distinctUntilChanged(),
map(state => state.toJS())
)
.subscribe(state => setTodos(state));
}, []);
useEffect(() => {
state$
.pipe(
map(state => state.get("todo")),
distinctUntilChanged(),
map(state => state.toJS())
)
.subscribe(state => setNewTodo(state));
}, []);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
{genList(todos)}
<div className="formGroup">
<input
type="text"
value={newTodo.title}
onChange={evt => actions.typeNewTodoTitle(evt.target.value)}
/>
<button
onClick = {() => {
actions.addTodo()
}}
>
Add TODO
</button>
<button
onClick={() => {
actions.undo();
}}
>UNDO</button>
</div>
</header>
</div>
);
};
Meisos
import { List, Record } from "immutable";
import { Subject } from "rxjs";
const model = {
initial: {
todo: Record({
title: "",
status: "PENDING"
})(),
todos: List([Record({ title: "Learn Meiosis", status: "PENDING" })()])
},
actions(update) {
return {
addTodo: (title, status = "PENDING") => {
update.next(state => {
console.log(title);
if (!title) {
title = state.get("todo").get("title");
}
const todo = Record({ title, status })();
return state.set("todos", state.get("todos").push(todo));
});
},
typeNewTodoTitle: (title, status = "PENDING") => {
update.next(state => {
return state.set("todo", Record({ title, status })())
});
},
resetTodo: () => {
update.next(state =>
state.set("todo", Record({ title: "", status: "PENDING" })())
);
},
removeTodo: i => {
update.next(state => state.set("todos", state.get("todos").remove(i)));
}
};
}
}
const update$ = new BehaviorSubject(state => state) // identity function to produce initial state
export const actions = model.actions(update$);
export default update$;
Solve my problem. It stemmed from a misunderstanding of how RXJS was working. An issue on the RxJS github page gave me the answer. Each subscriptions causes the observable pipeline to be re-evaluated. By adding the share operator to the pipeline it resolves this behavior.
export default update$.pipe(
scan(
(state, updater) =>
updater(state),
Record(initial)()
),
share()
);

Resources