React nested Hooks with state - reactjs

An extension of a question I was just answered but:
if you want to nest hooks that call state, is there a sane way of doing it, of should you just not do it?
For example:
const {useEffect, useState, useRef} = React;
const someSubHook = (setArtifactsStore) => {
const [match, setMatch] = useState("Hello");
const subhook = () => {
console.log("do some stuff with state");
setMatch("subhook")
};
return {match, sunhook};
};
const useRefreshArtifacts = (setArtifactsStore) => {
const [match, setMatch] = useState("Hello");
const refresh = () => {
console.log("do some stuff with state");
setMatch("refresh")
let h = someSubHook("sub");
h.subook()
};
return {match, refresh};
};
function ArtifactApp(props) {
const {match, refresh} = useRefreshArtifacts("");
return (<div><button onClick={refresh}>{match}</button></div>);
}
const AppContainer = () => {
return (<div><ArtifactApp /></div>);
}
ReactDOM.render(<AppContainer />, document.getElementById("root"));
https://jsfiddle.net/wu5ej3Lm/
Calling this throws an Invalid hook call. Hooks can only be called inside of the body of a function component.
Basically the codebase I have has a hook that performs some logic, and then has another hook that does some http post requests, both of which use state. I've inherited this so I'm finding my way through it, but I'm not sure if sticking all this as hooks is really best practice if there is a dependency on the state management.

Try to modify code in this way:
const {useEffect, useState, useRef} = React;
const someSubHook = (setArtifactsStore) => {
const [match, setMatch] = useState("Hello");
const subhook = () => {
console.log("do some stuff with state from subhook");
setMatch("subhook")
};
return {match, subhook};
};
const useRefreshArtifacts = (setArtifactsStore) => {
const [match, setMatch] = useState("Hello");
const {match_subhook, subhook} = someSubHook("sub");
const refresh = () => {
console.log("do some stuff with state from refresh");
setMatch("refresh")
subhook()
};
return {match, refresh};
};
function ArtifactApp(props) {
const {match, refresh} = useRefreshArtifacts("");
return (<div><button onClick={refresh}>{match}</button></div>);
}
const AppContainer = () => {
return (<div><ArtifactApp /></div>);
}
ReactDOM.render(<AppContainer />, document.getElementById("root"));
I have:
move let h = someSubHook("sub"); outside refresh function in this way const {match_subhook, subhook} = someSubHook("sub");
called subhook() in refresh function.
Here your fiddle modified.

Related

Every function called in `useEffect` stack must be wrapped in `useCallback`?

I am new to React and it seems to me that if you use a function inside of useEffect, that entire stack has to be wrapped in useCallback in order to comply with the linter.
For example:
const Foo = ({} => {
const someRef = useRef(0);
useEffect(() => {
startProcessWithRef();
}, [startProcessWithRef]);
const handleProcessWithRef = useCallback((event) => {
someRef.current = event.clientY;
}, []);
const startProcessWithRef = useCallback(() => {
window.addEventListener('mousemove', handleProcessWithRef);
}, [handleProcessWithRef]);
...
});
I'm wondering if there is a different pattern where I don't have to make the entire chain starting in useEffect calling startProcessWithRef be wrapped in useCallback with dependencies. I am not saying it is good or bad, I'm just seeing if there is a preferred alternative because I am new and don't know of one.
The idiomatic way to write your example would be similar to this:
Note the importance of removing the event listener in the effect cleanup function.
TS Playground
import {useEffect, useRef} from 'react';
const Example = () => {
const someRef = useRef(0);
useEffect(() => {
const handleMouseMove = (event) => { someRef.current = event.clientY; };
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, [someRef]);
return null;
};
If you prefer defining the function outside the effect hook, you'll need useCallback:
import {useCallback, useEffect, useRef} from 'react';
const Example = () => {
const someRef = useRef(0);
const updateRefOnMouseMove = useCallback(() => {
const handleMouseMove = (event) => { someRef.current = event.clientY; };
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, [someRef]);
useEffect(updateRefOnMouseMove, [updateRefOnMouseMove]);
return null;
};

React Hook access in an onclick function

I need to refresh a component using a button and a HTTP fetch request, so I've a hook that calls the function but I need to fire it from an onClick handler in a button:
const {useEffect, useState, useRef} = React;
const useRefreshArtifacts = (setArtifactsStore) => {
const refresh = () => {
console.log("do some stuff with state");
const [match, setMatch] = useState({});
};
return {refresh};
};
function ArtifactApp(props) {
const {refresh} = useRefreshArtifacts("");
return (<div><button onClick={refresh}>Hello</button></div>);
}
const AppContainer = () => {
return (<div><ArtifactApp /></div>);
}
ReactDOM.render(<AppContainer />, document.getElementById("root"));
https://jsfiddle.net/jre2cwbf/
Which gives the usual Invalid hook call. Hooks can only be called inside of the body of a function component but I can't figure out how to externalise the hook call, I've tried a useCallback but not got any close.
Any help appreciated.
Modify your code in this way:
const {useEffect, useState, useRef} = React;
const useRefreshArtifacts = (setArtifactsStore) => {
const [match, setMatch] = useState("Hello");
const refresh = () => {
console.log("do some stuff with state");
setMatch("refresh")
};
return {match, refresh};
};
function ArtifactApp(props) {
const {match, refresh} = useRefreshArtifacts("");
return (<div><button onClick={refresh}>{match}</button></div>);
}
const AppContainer = () => {
return (<div><ArtifactApp /></div>);
}
ReactDOM.render(<AppContainer />, document.getElementById("root"));
As you can see I have:
move useState outside refresh function;
set match on refresh function;
returned match from custom hook (useRefreshArtifacts) to use it on component.
Here your fiddle modified.

Convert class component function to a functional component function

I have class component functions that handle a search function.
filterData(offers,searchKey){
const result = offers.filter((offer) =>
offer.value.toLowerCase().includes(searchKey)||
offer.expiryDate.toLowerCase().includes(searchKey)||
offer.quantity.toLowerCase().includes(searchKey)
)
this.setState({offers:result})
}
const handleSearchArea = (e) =>{
const searchKey=e.currentTarget.value;
axios.get(`/viewPendingSellerOffers`).then(res=>{
if(res.data.success){
this.filterData(res.data.existingOffers,searchKey)
}
});
}
Now I try to convert these class component functions to functional component functions. To do this I tried this way.
const filterData = (offers,searchKey) => {
const result = offers.filter((offer) =>
offer.value.toLowerCase().includes(searchKey)||
offer.expiryDate.toLowerCase().includes(searchKey)||
offer.quantity.toLowerCase().includes(searchKey)
)
setState({offers:result})
}
const handleSearchArea = (e) =>{
const searchKey=e.currentTarget.value;
axios.get(`/viewPendingSellerOffers`).then(res=>{
if(res.data.success){
filterData(res.data.existingOffers,searchKey)
}
});
}
But I get an error that says "'setState' is not defined". How do I solve this issue?
Solution:
import React, {useState} from "react";
import axios from "axios";
const YourFunctionalComponent = (props) => {
const [offers, setOffers] = useState()
const filterData = (offersPara, searchKey) => {// I changed the params from offers to offersPara because our state called offers
const result = offersPara.filter(
(offer) =>
offer?.value.toLowerCase().includes(searchKey) ||
offer?.expiryDate.toLowerCase().includes(searchKey) ||
offer?.quantity.toLowerCase().includes(searchKey)
);
setOffers(result);
};
const handleSearchArea = (e) => {
const searchKey = e.currentTarget.value;
axios.get(`/viewPendingSellerOffers`).then((res) => {
if (res?.data?.success) {
filterData(res?.data?.existingOffers, searchKey);
}
});
};
return (
//To use your *offers* state object just call it like this {offers?.El1?.El2}
);
};
export default YourFunctionalComponent;
Note: It is recommended to do null check before accessing nested objects like this res?.data?.existingOffers, Optional chaining will help us in this regard.

React Hooks + Mobx => Invalid hook call. Hooks can only be called inside of the body of a function component

I have a React Native App,
Here i use mobx ("mobx-react": "^6.1.8") and react hooks.
i get the error:
Invalid hook call. Hooks can only be called inside of the body of a function component
Stores index.js
import { useContext } from "react";
import UserStore from "./UserStore";
import SettingsStore from "./SettingsStore";
const useStore = () => {
return {
UserStore: useContext(UserStore),
SettingsStore: useContext(SettingsStore),
};
};
export default useStore;
helper.js OLD
import React from "react";
import useStores from "../stores";
export const useLoadAsyncProfileDependencies = userID => {
const { ExamsStore, UserStore, CTAStore, AnswersStore } = useStores();
const [user, setUser] = useState({});
const [ctas, setCtas] = useState([]);
const [answers, setAnswers] = useState([]);
useEffect(() => {
if (userID) {
(async () => {
const user = await UserStore.initUser();
UserStore.user = user;
setUser(user);
})();
(async () => {
const ctas = await CTAStore.getAllCTAS(userID);
CTAStore.ctas = ctas;
setCtas(ctas);
})();
(async () => {
const answers = await AnswersStore.getAllAnswers(userID);
UserStore.user.answers = answers.items;
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
setAnswers(answers.items);
})();
}
}, [userID]);
};
Screen
import React, { useEffect, useState, useRef } from "react";
import {
View,
Dimensions,
SafeAreaView,
ScrollView,
StyleSheet
} from "react-native";
import {
widthPercentageToDP as wp,
heightPercentageToDP as hp
} from "react-native-responsive-screen";
import { observer } from "mobx-react";
import useStores from "../../stores";
import { useLoadAsyncProfileDependencies } from "../../helper/app";
const windowWidth = Dimensions.get("window").width;
export default observer(({ navigation }) => {
const {
UserStore,
ExamsStore,
CTAStore,
InternetConnectionStore
} = useStores();
const scrollViewRef = useRef();
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
if (InternetConnectionStore.isOffline) {
return;
}
Tracking.trackEvent("opensScreen", { name: "Challenges" });
useLoadAsyncProfileDependencies(UserStore.userID);
}, []);
React.useEffect(() => {
const unsubscribe = navigation.addListener("focus", () => {
CTAStore.popBadget(BadgetNames.ChallengesTab);
});
return unsubscribe;
}, [navigation]);
async function refresh() {
const user = await UserStore.initUser(); //wird das gebarucht?
useLoadAsyncProfileDependencies(UserStore.userID);
if (user) {
InternetConnectionStore.isOffline = false;
}
}
const name = UserStore.name;
return (
<SafeAreaView style={styles.container} forceInset={{ top: "always" }}>
</SafeAreaView>
);
});
so now, when i call the useLoadAsyncProfileDependencies function, i get this error.
The Problem is that i call useStores in helper.js
so when i pass the Stores from the Screen to the helper it is working.
export const loadAsyncProfileDependencies = async ({
ExamsStore,
UserStore,
CTAStore,
AnswersStore
}) => {
const userID = UserStore.userID;
if (userID) {
UserStore.initUser().then(user => {
UserStore.user = user;
});
CTAStore.getAllCTAS(userID).then(ctas => {
console.log("test", ctas);
CTAStore.ctas = ctas;
});
AnswersStore.getAllAnswers(userID).then(answers => {
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
});
}
};
Is there a better way? instead passing the Stores.
So that i can use this function in functions?
As the error says, you can only use hooks inside the root of a functional component, and your useLoadAsyncProfileDependencies is technically a custom hook so you cant use it inside a class component.
https://reactjs.org/warnings/invalid-hook-call-warning.html
EDIT: Well after showing the code for app.js, as mentioned, hook calls can only be done top level from a function component or the root of a custom hook. You need to rewire your code to use custom hooks.
SEE THIS: https://reactjs.org/docs/hooks-rules.html
You should return the value for _handleAppStateChange so your useEffect's the value as a depdendency in your root component would work properly as intended which is should run only if value has changed. You also need to rewrite that as a custom hook so you can call hooks inside.
doTasksEveryTimeWhenAppWillOpenFromBackgorund and doTasksEveryTimeWhenAppGoesToBackgorund should also be written as a custom hook so you can call useLoadAsyncProfileDependencies inside.
write those hooks in a functional way so you are isolating specific tasks and chain hooks as you wish without violiating the rules of hooks. Something like this:
const useGetMyData = (params) => {
const [data, setData] = useState()
useEffect(() => {
(async () => {
const apiData = await myApiCall(params)
setData(apiData)
})()
}, [params])
return data
}
Then you can call that custom hook as you wish without violation like:
const useShouldGetData = (should, params) => {
if (should) {
return useGetMyData()
}
return null
}
const myApp = () => {
const myData = useShouldGetData(true, {id: 1})
return (
<div>
{JSON.stringify(myData)}
</div>
)
}

Test react hooks state using Jest and React Hooks Library

I nav component then will toggle state in a sidebar as well as open and close a menu and then trying to get this pass in code coverage. When I log inside my test my state keeps showing up as undefined. Not sure how to tackle this one here.
Component.js:
const Navigation = (props) => {
const {
classes,
...navProps
} = props;
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return (
<Fragment>
<Button
onClick={toggleMenuOpen}
/>
<SideMenu
toggleSidebar={toggleSidebar}
>
<Menu
onClose={toggleMenuClose}
>
</SideMenu>
</Fragment>
);
};
export default Navigation;
Test.js:
import { renderHook, act } from '#testing-library/react-hooks';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => Navigation({ ...newProps }));
expect(result.current.sidebarOpen).toBeFalsy();
});
Author of react-hooks-testing-library here.
react-hooks-testing-library is not for testing components and interrogating the internal hook state to assert their values, but rather for testing custom react hooks and interacting withe the result of your hook to ensure it behaves how you expect. For example, if you wanted to extract a useMenuToggle hook that looked something like:
export function useMenuToggle() {
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return {
sidebarOpen,
toggleSidebar,
toggleMenuClose,
toggleMenuOpen
}
}
Then you could test it with renderHook:
import { renderHook, act } from '#testing-library/react-hooks';
// Hooks
import { useMenuToggle } from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => useMenuToggle());
expect(result.current.sidebarOpen).toBeFalsy();
act(() => {
result.current.toggleSidebar()
})
expect(result.current.sidebarOpen).toBeTruthy();
});
Generally though, when a hook is only used by a single component and/or in a single context, we recommend you simply test the component and allow the hook to be tested through it.
For testing your Navigation component, you should take a look at react-testing-library instead.
import React from 'react';
import { render } from '#testing-library/react';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { getByText } = render(<Navigation {...newProps} />);
// the rest of the test
});

Resources