React+Jest Testing state change in useEffect - reactjs

So I've a similar useCase defined in stackover flow answer, but somewhat different. I have a react component and it has two useEffect hooks. One useEffect hook is used at first time render and other once the dependency in that useEffect hook changes. I tried mocking getDelivery but it comes from a custom hook defined in app.
// App.tsx
const App = () => {
const [deliveryType, setDeliveryType] = useState([])
const { DeliveryStatusService } = useServices()
const deliveryStatusService = new DeliveryStatusService()
const { deliveryId } = useParams()
useEffect(() => {
setup()
}, [])
useEffect(() => {
if (deliveryType) {
console.log("DeliveryType is Set")
}
}, [deliveryType])
const setup = async () => {
const response = await deliveryStatusService.getDelivery(deliveryId) // this call comes from custom hook and external service
setDeliveryType(response)
}
return (
<>
<div>{!deliveryType ? <div>Render Loading</div> : <div>Data loaded with {deliveryType}</div>}</div>
</>
)
}
I tried to mock it out as stated in above article as follows using Jest and Enzyme but if I console.log(wrapper.debug) I still get Render Loading and not Data loaded with {deliveryType}.
I tried mocking it like this :
const getDelivery = jest.fn()
const DeliveryStatusService = jest.fn().mockImplementation(() => ({
getDelivery,
}))
it('mock getDelivery', () => {
const wrapper = mount(<ServiceProvider services={{DeliveryStatus}}>
<App />
</ServiceProvider>
getDelivery.mockReturnValue(
Promise.resolve(
... dummyData
),
)
await act(flushPromises)
wrapper.update()
await act(flushPromises)
console.log(wrapper.debug())
console.log(wrapper.debug()) yields the following output:
<ServiceProvider services={{...}}>
<App>
<div>
<div>
Render Loading
</div>
</div>
<App>
</ServiceProvider>
Am I not mocking correctly so that I would never have Data loaded div?

Related

Initializing component with parameter from local storage in react/nextjs

I have a component provided by an external library which I need to provide an access token to. This access token is saved in localStorage at this point and I would like to read it and pass it to the component. This is my attempt using useEffect but this fails with the error {code: "initialization-error", error: "Access token required"}
const test: NextPage = () => {
const router = useRouter()
let accessToken = ''
useEffect(() => {
const at = localStorage.getItem('accessToken')
console.log(at) // prints out the correct access token
if (at !== null) {
accessToken = at
}
}, [])
return (
<WebSdk
accessToken={accessToken}
expirationHandler={() => Promise.resolve(accessToken)}
onError={(error) => {
console.log(error)
}}
/>
)
}
export default test
I'm guessing the useEffect hook runs after the component renderes - how can I read from local storage before or as the component renders?
Since you are using nextjs you can't call the localStorage directly, so you have to check if the bowser has "initialized", something like this:
const test: NextPage = () => {
const router = useRouter()
const [initialized, setInitialized] = useState(false);
const [accessToken, setAccessToken] = useState('');
useEffect(() => {
const at = localStorage.getItem('accessToken') || '';
setInitialized(true);
setAccessToken(at);
}, [])
if (!initialized) {
return null;
}
return (
<WebSdk
accessToken={accessToken}
expirationHandler={() => Promise.resolve(accessToken)}
onError={(error) => {
console.log(error)
}}
/>
)
}
export default test
I'm guessing the useEffect hook runs after the component renderes
Yes the component will render first before the hooks can be triggered, this is the equivalent of "componentDidMount" (if the hook's second parameter is empty) in early verions of react.

Cannot read property 'displayName' of undefined. React+Jest+Enzyme

I am rendering a react Component App.tsx which uses useEffect hook. Inside the useEffect, it makes an async call updates the state delivery defined in App.tsx. At first render, delivery is undefined, but after next re-render, it is defined.
const App = () => {
const [delivery, setDelivery] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setup()
}, [])
const setup = async () => {
try {
const response = await someAsyncCall()
setDelivery(response)
setLoading(true)
/// Other functionality
} catch (err) {
console.log(err)
}finally{
setLoading(false)
}
}
return (
<>
{loading? <div>Loading!!</div>
: <div>App has loaded with {delivery.displayName} {delivery.id}</div>
}
</>
)
}
How to write unit test so that it re-renders with right value? Right now I get Uncaught [TypeError: Cannot read property 'displayName' of undefined]
Unit Test I wrote :
describe('test', ()=>{
it("mounts", ()=>{
const wrapper = mount(<App />)
})
})
Any thoughts on how to re-render? I read wrapper.update() could be used. I was wondering how?
Shouldn't this solve the problem?
return (
<>
{loading || !delivery ? <div>Loading!!</div>
: <div>App has loaded with {delivery.displayName} {delivery.id}</div>
}
</>
)

please explain the error to me, Error: Rendered more hooks than during the previous render

Iam newbie and now learning to make customize react hooks
here i am trying to call the function i made in app.js file, i want to use it onClick button. but fail to do so. please help me to find the error and understand it.
import React, {
useEffect,
useState
} from "react";
const useRandomJoke = () => {
const [jokes, setJokes] = useState();
useEffect(() => {
const jokeFetch = async() => {
await fetch("https://api.icndb.com/jokes/random")
//we'll run 2 "then"
.then(
// this will give us response and will return inform of res.json
(res) => res.json()
) //.json is a format
.then((data) => {
setJokes(data.value.joke);
}); // now calling data from te returned values in res.json
};
jokeFetch();
}, []);
return jokes;
};
export default useRandomJoke;
//With onClick function
function App() { const [jokes, setJokes] = useState();
return (
<div className="App">
<h1>Random Jokes</h1>
<p>{jokes}</p>
<button onClick={()=>{setJokes(useRandomJoke)}}>
Click for Jokes</button>
</div>
); } export default App;
`
useRandomJoke is a custom hook. Hooks should only be called at the top level of a component and as the custom hook already has the joke state, you don't need an additional state in the App component.
If you want to get a new joke after the component renders and every time the button gets clicked, you can do this:
const useRandomJoke = () => {
const [joke, setJoke] = useState("");
const fetchJoke = useCallback(() => {
fetch("https://api.icndb.com/jokes/random")
.then((res) => res.json())
.then((data) => {
setJoke(data.value.joke);
});
}, []);
return [joke, fetchJoke];
};
export default function App() {
const [joke, fetchJoke] = useRandomJoke();
useEffect(() => {
fetchJoke();
}, [fetchJoke]);
return (
<div className="App">
<h1>Random Jokes</h1>
<p>{joke}</p>
<button onClick={fetchJoke}>Click for a random joke</button>
</div>
);
}
You can't conditionally call React hooks, like in the onClick handler of the button, as this breaks the rules of hooks. I suggest refactoring the useRandomJoke hook to return the fetched joke and a function to fetch the next random joke. You also shouldn't mix async/await with Promise chains as this is an anti-pattern.
const useRandomJoke = () => {
const [jokes, setJokes] = useState(null);
const jokeFetch = async () => {
const res = await fetch("https://api.icndb.com/jokes/random");
const data = await res.json();
setJokes(data.value.joke)
};
return [jokes, jokeFetch];
};
Then use the hook in the app.
function App() {
const [joke, getJoke] = useRandomJoke();
return (
<div className="App">
<h1>Random Jokes</h1>
<p>{joke}</p>
<button onClick={getJoke}>Click for Joke</button>
</div>
);
}
Well, there is more than one point to talk about here:
1- in React.js, you can only call custom hooks at the top level of your function's body (react recognizes any function starting with the keyword use as a hook)
function App() {
// top level is here
const randomJokes = useRandomJoke()
const [jokes, setJokes] = useState();
return (
<div className="App">
<h1>Random Jokes</h1>
<p>{jokes}</p>
<button onClick={()=>{setJokes(useRandomJoke)}}>
Click for Jokes
</button>
</div>
); }
export default App;
2- In your example I understand you want to have a new joke each time onClick triggers, in order to do so, I don't think using a custom hook is the ideal solution here, since your custom hook runs the fetchJokes method only once on initial render (as you described in your useEffect hook), I understand a lot of people mention that useEffect is the place to make API calls, but it doesn't necessarily applies to all use cases, in your example it is simple, you don't have to use useEffect neither create a custom hook.
a possible simple solution:
function App() {
// we always call hooks at the top level of our function
const [jokes, setJokes] = useState();
const fetchNewJoke = () => {
fetch("https://api.icndb.com/jokes/random")
//we'll run 2 "then"
.then(
// this will give us response and will return inform of
res.json
(res) => res.json()
) //.json is a format
.then((data) => {
setJokes(data.value.joke);
}); // now calling data from te returned values in res.json
};
};
return (
<div className="App">
<h1>Random Jokes</h1>
<p>{jokes}</p>
<button onClick={fetchNewJoke}>Click for Joke</button>
</div>
);
} export default App;

React Native data in context is undefined on the first render

I use AppContext, when I fetch data from server I want it to save in context but on the first render it doesn't save. If I make something to rerender state data appears in context.
Here is my code:
useEffect(() => {
fetch('https://beautiful-places.ru/api/places')
.then((response) => response.json())
.then((json) => myContext.updatePlaces(json))
.then(() => console.log('jsonData', myContext.getPlaces()))
.catch((error) => console.error(error));
}, []);
My getPlaces and updatePlaces methods:
const [allPlaces, setAllPlaces] = useState();
const getPlaces = () => {
return allPlaces;
};
const updatePlaces = (json) => {
setAllPlaces(json);
};
const placesSettings = {
getPlaces,
updatePlaces,
};
Here is how I use AppContext:
<AppContext.Provider value={placesSettings}>
<ThemeProvider>
<LoadAssets {...{ assets }}>
<SafeAreaProvider>
<AppStack.Navigator headerMode="none">
<AppStack.Screen
name="Authentication"
component={AuthenticationNavigator}
/>
<AppStack.Screen name="Home" component={HomeNavigator} />
</AppStack.Navigator>
</SafeAreaProvider>
</LoadAssets>
</ThemeProvider>
</AppContext.Provider>;
Could you explain please why my console.log('jsonData', ...) returns undefined?
I don't understand because on previous .then I saved it.
Edit to note that the code below is not copy-paste ready. It is an example of how to attack the problem – you will need to implement it properly in your project.
The 'problem' is that hooks are asynchronous – in this specific case, your useEffect further uses an asynchronous fetch too.
This means that the data that is returned by the fetch will only be available after the component has rendered, and because you're not updating state/context using a hook, the context won't update.
The way to do this requires a few changes.
In your context implementation, you should have a setter method that sets a state variable, and your getter should be that state variable.
placesContext.js
import React, { createContext, useState } from "react";
export const placesContext = createContext({
setPlaces: () => {},
places: [],
});
const { Provider } = placesContext;
export const PlacesProvider = ({ children }) => {
const [currentPlaces, setCurrentPlaces] = useState(unit);
const setPlaces = (places) => {
setCurrentPlaces(places);
};
return (
<Provider value={{ places: currentPlaces, setPlaces }}>{children}</Provider>
);
};
Wrap your App with the created Provider
App.js
import { PlacesProvider } from "../path/to/placesContext.js";
const App = () => {
// ...
return (
<PlacesProvider>
// Other providers, and your app Navigator
</PlacesProvider>
);
}
Then, you should use those variables directly from context.
MyComponent.js
import { placesContext } from "../path/to/placesContext.js";
export const MyComponent = () => {
const { currentPlaces, setPlaces } = useContext(placesContext);
const [hasLoaded, setHasLoaded] = useState(false);
useEffect(() => {
async function fetchPlacesData() {
const placesData = await fetch('https://beautiful-places.ru/api/places');
if (placesData) {
setPlaces(placesData);
} else {
// error
}
setHasLoaded(true);
}
!hasLoaded && fetchPlacesData();
}, [hasLoaded]);
return (
<div>{JSON.stringify(currentPlaces)}</div>
)
};

Are there any potential issues with passing a custom react hook as a prop

I have a custom react hook that is used by a component but rather than getting the hook from the global context I'd like to define it in the props as below.
const useThingFn = () => {
const doThing = () => console.log('Did thing')
const data = 'hello'
return { data, doThing }
}
const Component = ({
// Hooks
useThing = useThingFn,
}) => {
const { data, doThing } = useThing();
return (
<div>
<button
onClick={doThing}
>
Click Me
</button>
{data}
</div>
);
};
Are there any potential issues with this? The purpose would be to easily mock the hooks in a test.

Resources