How to make Jest test for hook without UI - reactjs

I have the next hook
const useFetch => {
let { state, dispatch } = useContext(AppContext);
const fetch1 = async () => {
...code to fetch data (mocked)
}
const fetch2 = () => {
...
}
return {
fetch1,
fetch2,
...
};
};
export default useFetch;
I need to create jest test but i can't do it directly because it break the rules of hooks, and I can't render it because it not have UI to render, it's only fetch data to store to the context.
Can anyone help me to understand how to do it?
Thanks in advance and sorry for my english.

You could create a wrapper component for test purpose only and work with that. However Testing Library has a great tool for that, hooks-testing-library. It might be a good addition to your project if you have more hooks to test.

Related

Why "React hook is called in a function" while exporting simple single function that accesses a useAtom state?

I have this simple function that needs to set a state from jotai. I would like this function to have it's own seperate file or be cleaned up somewhere, since it will be reused. I'm new to React and come from Angular. It's kind of a function in a service Angular wise.
How would you solve this properly in React?
Code:
export const setMetamaskWallet = (): void => {
const [, setWeb3] = useAtom(web3Atom);
const [, setLoading] = useAtom(loadingAtom);
const [wallet, setWallet] = useAtom(walletAtom);
setWeb3(
new Web3(window.ethereum),
)
//Todo: Create check if metamask is in browser, otherwise throw error
const setAccount = async () => {
setLoading(true);
const accounts = await window.ethereum.request(
{
method: 'eth_requestAccounts'
},
);
setWallet(
accounts[0],
);
setLoading(false);
}
if (!wallet) {
setAccount();
}
}
Hooks can only be called in React functional components or other hooks. Here it appears you have called it in a typical function, hence the error. You could package this functionality in a custom hook. It may however be most appropriate to keep this function as it is, and instead of calling hooks within it, pass the relevant data into the function as parameters.

Is there a way in React to test whether or not a Functional Component calls a particular custom hook?

describe("UI Logic", () => {
it("Uses useEmailMultiSelectCustomHook", () => {
const component = render(<EmailMultiSelectCombobox/>);
const hook = jest.mock("./useEmailMultiSelectCombobox", () => ({
useEmailMuliSelect: () => {
return {
initialConditions: ["test", "test1", "test2"]
}
}
}));
const hookTest = renderHook(useEmailMultiSelectCombobox);
expect(hookTest).toHaveBeenCalledTimes(1);
});
})
export const EmailMultiSelectCombobox = () => {
const { initialConditions } = useEmailMultiSelectCombobox();
return (
<>
{/*<MultiSelectCombobox initialConditions={} handleSelectionChange={}/>*/}
</>
)
};
export const useEmailMultiSelectCombobox = () => {
const [initialConditions, setInitialConditions] = useState<Array<string>>([]);
return { initialConditions };
}
So, I have a superrrr basic setup here. All I'm trying to do is test whether or not the hook was called/initialized from the component that I want to use the custom hook. The idea here is to be able to truly unit test a custom hook apart from unit testing a view. Most things I've seen really involve an integration test with a component view layer and a component logic layer being tested together, using the user-events library that comes with the testing library.
I only have questionable answers for you which aren't really great in production, but for testing - could be acceptable?
Here's some arcane knowledge that might help. Keep in mind this is a fairly cursed abuse of power, but it might work for your purposes.
Option 1
All functions in JavaScript have a secret way to extract the code that was used to make them. Here's an example:
function MyComponent({ id, ...props }) {
const [clicked, setClicked] = useState(false);
return <div onClick={setClicked.bind(this,true)} id={id} {...props}>Hello, world</div>;
}
console.log({ usesState: MyComponent.toString().includes("useState"));
There are some nuances here. For example: if your component uses styled-components or really any kind of composition (like connect() with redux) this might not work. But if the component is pure, it should meet your needs.
I want to emphasize this is a fairly questionable use of function.toString().
Option 2
Also a bad idea, but you could have your hook in question set some test flag somewhere and then check for the presence of it.
function useMyHook() {
window.useMyHookTriggered = true;
}
if (window.useMyHookTriggered) {
// Hook was used
}
This is not great because it pollutes the public namespace. But again, it'd work.
Option 3
Your hook could emit an event. This solution, I don't hate. Still kinda pollutes things, but in a sane way.
function useMyHook() {
window.dispatchEvent(new Event("useMyHookInvoked"));
}
// The test
window.addeventlistener("useMyHookInvoked", () => {
assert(true);
})

why react hook init before redux props

this is some kind of code so be gentle :D
i have some page that use react hooks and redux and the redux state work
i checked it it load data. the scenario is i have page that load input boxes from redux thunk and save the changes with hooks and redux thunk
const MyComponent = (props) => {
const { reduxProp } = props
const [t1, setT1] = useState('')
useEffect( () => {
reduxActionLoadData // t1 , t2 data
setT1(reduxProp.t1)
setT2(reduxProp.t2)
},[])
return (
<div>
<input value={t1} onChange={ (e) => { setT1(e.target.value) } />
<input value={t2} onChange={ (e) => { setT2(e.target.value) } />
</div>
)
}
const mapStateToProp = (state) => (
{
reduxProp: state.reduxProp
})
const mapDispatchToProp = (dispatch) => (
{
reduxActionLoadData = connectActionCreators(reduxActionLoadDataAction, dispatch)
})
export default const connect(mapStateToProps, mapDispatchToProps)(MyComponent)
when i check props it is loaded, redux state is loaded too
but hooks init with empty value
i try to init them with reduxProp.t1, reduxProp.t2 no luck there too
the thing is when i run app it works fine but when i refresh the page it fails
and sometimg it fails when i run app too
i try to use loading state and do stuff after loading = true no luck the either
For one, your mapDispatchToProp currently does not seem to do anything, since with the = it will be parsed as a method body, not as an object - so you are not connecting your actions at all. It should look more like this:
const mapDispatchToProp = (dispatch) => (
{
reduxActionLoadData: connectActionCreators(reduxActionLoadDataAction, dispatch)
})
Then you should instead be using the map object notation:
const mapDispatchToProp = {
reduxActionLoadData: reduxActionLoadDataAction
}
and the you also have to actually use that in your component:
useEffect( () => {
props.reduxActionLoadData()
},[])
That will still create an asynchronous effect, so reduxProp.t1 will not change immediately.
You will probably have to update those later in a separate useEffect.
useEffect( () => {
setT1(reduxProp.t1)
},[reduxProp.t1])
All that said, there is no reason to use connect at all, since you could also use the useSelector and useDispatch hooks. connect is a legacy api that you really only need to use with legacy class components.
Generally I would assume that you are right now learning a very outdated style of Redux by following an outdated tutorial. Modern Redux does not use switch..case reducers, ACTION_TYPE constants, hand-written action creators, immutable reducer logic or connect - and it is maybe 1/4 of the code of old-style Redux.
Please follow the official Redux tutorial for an understanding of mondern Redux. https://redux.js.org/tutorials/essentials/part-1-overview-concepts

React useEffect inside const function with MobX

I have some React Redux code written in Typescript that loads some data from my server when a component mounts. That code looks like this:
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { MyAction } from 'my/types/MyAction';
export const useDispatchOnMount = (action: MyAction) => {
const dispatch = useDispatch();
return useEffect(() => {
dispatch(action);
}, [dispatch]);
};
This is simple enough - it uses the useEffect hook to do what I want. Now I need to convert the code so that it uses MobX instead of Redux for persistent state. If I have my own MobX store object called myStore, and myStore has a async method "loadXYZ" that loads a specific set of data XYZ, I know I can do this inside my component:
useEffect(() => {
async functon doLoadXYZ() {
await myStore.loadXYZ();
}
doLoadXYZ();
}, []);
This does indeed work, but I would like to put all this into a single fat arrow function that calls useEffect, much like what the useDispatchOnMount function does. I can't figure out the best way to do this. Anyone know how to do this?
EDIT: After further digging, it looks more and more like what I am trying to do with the Mobx version would break the rules of Hooks, ie always call useEffect from the top level of the functional component. So calling it explicitly like this:
export const MyContainer: React.FC = () => {
useEffect(() => {
async functon doLoadXYZ() {
await myStore.loadXYZ();
}
doLoadXYZ();
}, []);
...
};
is apparently the best way to go. Butthat raises the question: is the redux version that uses useDispatchOnMount a bad idea? Why?
You can do this if you don't use async/await in the useEffect. If you are fetching data, I would store it in myStore and use it directly out of the store instead of using async/await. It might look something like this:
export const SomeComp = observer(function SomeComp() {
const myStore = useStore() // get the store with a hook or how you are getting it
useEffect(myStore.loadXYZ, [myStore])
return <div>{myStore.theLoadedData}</div>
})
In loadXYZ you just store the data the way you want and use it. The component observing theLoadedData will re-render when it comes back so you don't need to have async/await in the component.

How to do fetch with React Hooks; ESLint enforcing `exhaustive-deps` rule, which causes infinite loop

I'm pretty new to React hooks in general, and very new to useSelector and useDispatch in react-redux, but I'm having trouble executing a simple get request when my component loads. I want the get to happen only once (when the component initially loads). I thought I knew how to do that, but I'm running into an ESLint issue that's preventing me from doing what I understand to be legal code.
I have this hook where I'm trying to abstract my state code:
export const useState = () => {
const dispatch = useDispatch();
const data = useSelector((state) => state.data);
return {
data: data,
get: (props) => dispatch(actionCreators.get(props))
};
};
Behind the above function, there's a network request that happens via redux-saga and axios, and has been running in production code for some time. So far, so good. Now I want to use it in a functional component, so I wrote this:
import * as React from 'react';
import { useState } from './my-state-file';
export default () => {
const myState = useState();
React.useEffect(
() => {
myState.get();
return () => {};
},
[]
);
return <div>hello, world</div>;
};
What I expected to happen was that because my useEffect has an empty array as the second argument, it would only execute once, so the get would happen when the component loaded, and that's it.
However, I have ESLint running on save in Atom, and every time I save, it changes that second [] argument to be [myState], the result of which is:
import * as React from 'react';
import { useState } from './my-state-file';
export default () => {
const myState = useState();
React.useEffect(
() => {
myState.get();
return () => {};
},
[myState]
);
return <div>hello, world</div>;
};
If I load this component, then the get runs every single render, which of course is the exact opposite of what I want to have happen. I opened this file in a text editor that does not have ESLint running on save, so when I was able to save useEffect with a blank [], it worked.
So I'm befuddled. My guess is the pattern I'm using above is not correct, but I have no idea what the "right" pattern is.
Any help is appreciated.
Thanks!
UPDATE:
Based on Robert Cooper's answer, and the linked article from Dan Abramov, I did some more experimenting. I'm not all the way there yet, but I managed to get things working.
The big change was that I needed to add a useCallback around my dispatch functions, like so:
export const useState = () => {
const dispatch = useDispatch();
const data = useSelector((state) => state.data);
const get = React.useCallback((props) => dispatch({type: 'MY_ACTION', payload:props}), [
dispatch
]);
return {
data: data,
get: get,
};
};
I must admit, I don't fully understand why I need useCallback there, but it works.
Anyway, then my component looks like this:
import * as React from 'react';
import { useState } from './my-state-file';
export default () => {
const {get, data} = useState();
React.useEffect(
() => {
get();
return () => {};
},
[get]
);
return <div>{do something with data...}</div>;
};
The real code is a bit more complex, and I'm hoping to abstract the useEffect call out of the component altogether and put it into either the useState custom hook, or another hook imported from the same my-state-file file.
I believe the problem you're encountering is that the value of myState in your dependency array isn't the same value or has a different JavaScript object reference on every render. The way to get around this would be to pass a memoized or cached version of myState as a dependency to your useEffect.
You could try using useMemo to return a memoized version of your state return by your custom useState. This might look something like this:
export const useState = () => {
const dispatch = useDispatch();
const data = useSelector((state) => state.data);
return useMemo(() => ({
data: data,
get: (props) => dispatch(actionCreators.get(props))
}), [props]);
};
Here's what Dan Abramov has to say regarding infinite loops in useEffect methods:
Question: Why do I sometimes get an infinite refetching loop?
This can happen if you’re doing data fetching in an effect without the second dependencies argument. Without it, effects run after every render — and setting the state will trigger the effects again. An infinite loop may also happen if you specify a value that always changes in the dependency array. You can tell which one by removing them one by one. However, removing a dependency you use (or blindly specifying []) is usually the wrong fix. Instead, fix the problem at its source. For example, functions can cause this problem, and putting them inside effects, hoisting them out, or wrapping them with useCallback helps. To avoid recreating objects, useMemo can serve a similar purpose.
Full article here: https://overreacted.io/a-complete-guide-to-useeffect/

Resources