ReactJS - Test conditional rendering in component - reactjs

I have a component the uses useEffect to fetch data from a file.
In the component i have a condiiton that only shows the content of the component if we have data.
Now how can a test the conditional part of the content i my test case?
This is what i have right now:
Component:
function MunicipalityInfo() {
const [municipalityData, setMunicipalityData] = useState({})
const fetchData = async () => {
try{
const result = await fetch(XMLFile)
const data = await result.text();
const xml = new XMLParser().parseFromString(data);
const res = XMLMapper(xml)
setMunicipalityData(res)
}catch(e){
console.log(e)
}
}
useEffect(() => {
fetchData();
}, []);
return(
<>
{ municipalityData.units &&
municipalityData.units.map((city, index) => {
return (
<Div key={index} data-testid="municipalityInfo-component" className="mt-5 p-3">
<HeaderMain data-testid="header-main">{city.City}</HeaderMain>
<HeaderSub data-testid="header-sub" className="mt-4">{city.venamn}</HeaderSub>
<BodyText data-testid="body-text">{city.Address}, {city.City}</BodyText>
<MapLink href={"#"} data-testid="map-link"><i data-testid="map-icon" className="fas fa-map-marker-alt"></i> Show on map</MapLink>
<LinkList data-testid="link-list">
<LinkListItem data-testid="list-item-first">
<Link href={city.BookingURL} data-testid="link-book-vaccination">Some text</Link>
</LinkListItem>
</LinkList>
<Calendar data={city.unit}/>
</Div>
)
})
}
<CitiesSideBar>
<Sidebar data={municipalityData.cities}/>
</CitiesSideBar>
</>
)
}
export default MunicipalityInfo;
And this is my test:
describe("<MunicipalityInfo />", () => {
it("renders without crashing", async () => {
const {queryByTestId, findByText, findByTestId} = render(<MunicipalityInfo/>, {})
expect(queryByTestId("municipalityInfo-component")).not.toBeInTheDocument();
expect(await findByTestId("municipalityInfo-component")).toBeInTheDocument(); <--- this line fails
})
})
And the error of my testcase:
TestingLibraryElementError: Unable to find an element by: [data-testid="municipalityInfo-component"]

if your problem is trying to test if something shouldn't be in the page...
use the queryBy
if you're want it to wait for something... then you want to await findBy (or wrap in a waitFor)
here's the docs: https://testing-library.com/docs/react-testing-library/cheatsheet/
I'm assuming you're mocking the fetch request so it wouldn't be the test problem...
if you're not mocking it... then you probably should mock and return either data or no data to test if it should or not render.
one way to elegantly "avoid" mocking would be by abstracting it in a custom hook:
function useCustomHook(){
const [municipalityData, setMunicipalityData] = useState({})
useEffect(() => {
fetch(XMLData)
.then((res) => res.text())
.then(async (data) => {
let xml = new XMLParser().parseFromString(data);
let result = await XMLMapper(xml)
setMunicipalityData(await result)
})
.catch((err) => console.log(err));
}, []);
return municipalityData;
}
function MunicipalityInfo({municipalityData = useCustomHook()}) { ... }
then in the test you can simply
render(<MunicipalityInfo municipalityData={'either null or some mocked data'} />)

Related

React prevent re render

How should I prevent a re render when I click on onClose to close the modal.
It looks like the dispatch function: dispatch(setActiveStep(0) cause some troubles.
export default function ImportOrderModal(props: ImportOrderModalProps) {
const { open, onClose } = props
const {
orderImport: { activeStep }
} = useAppSelector((state: RootState) => state)
const steps = useImportOrderConfig()
const dispatch = useDispatch()
React.useEffect(() => {
getOrdersList()
}, [])
const onCloseModal = () => {
onClose()
// Force a re render because of activeStep value
dispatch(setActiveStep(0))
}
const getOrdersList = async () => {
const orders = //API call
dispatch(setOrdersList(orders))
}
return (
<Modal open={open} onClose={onCloseModal}>
<Stepper steps={steps} currentStepNumber={activeStep} />
<FormSteps />
</Modal>
)
}
This block is outside of your useEffect()
const getOrdersList = async () => {
const orders = //API call
dispatch(setOrdersList(orders))
}
This will cause rendering troubles.
if you're using an older version of React (<17) that doesn't enforce <React.StrictMode> you can get away with rewriting that as:
useEffect(() => {
getOrderList
.then((orders) => dispatch(setOrdersList(orders))
.catch((error) => console.error(error));
}, [dispatch]);
if you're using a newer version of React (>18) you will have to cleanup your asynchronous call in the cleanup function of your useEffect().
useEffect(() => {
// This has to be passed down to your fetch/axios call
const controller = new AbortController();
getOrderList(controller)
.then((orders) => dispatch(setOrdersList(orders))
.catch((error) => console.error(error));
return () => {
// This will abort any ongoing async call
controller.abort();
}
}, [dispatch]);
For this to make sense I will probably have to write an example of the api call for you as well, if you don't mind I'll use axios for the example but it essentially works the same-ish with .fetch.
const getOrderList = async (controller) => {
try {
const { data } = await axios.get("url", { signal: controller.signal });
return data.orders;
} catch (e) {
throw e;
}
}

React.js with fetch api and console.log()

I can't log or display data in this barebones React component. I am trying to just simply fetch my repos using the fetch api. I am getting a response back when I look at Network tab in dev tools.
I tried to wrap the call in useEffect() (then storing the data from the response into a state variable) - that didn't work so that's why I have this barebones component for now.
const Component = () => {
const [repos, setRepos] = useState([])
useEffect(() => {
// fetch call used to be here
}, [])
const data = fetch('https://api.github.com/users/alexspurlock25/repos')
.then(response => response.json())
.then(data => setRepos(data))
console.log(data)
console.log(repos)
return (
<div>
{
repos.map(items => console.log(items))
}
</div>
)
}
Why can't I log or map the data? Am I doing something wrong?
Create an async function that handles the api call. Then call the function in the useEffect. Since Repos is an empty array, nothing will be logged. Once your api call resolves and the repos state has been updated, react will do it's thing and re render causing the repos.map to run again and log out the repos
const Component = () => {
const [repos, setRepos] = useState([])
const fetchData = async ()=>{
let res = await fetch('https://api.github.com/users/alexspurlock25/repos')
let data = await res.json()
setRepos(data)
}
useEffect(() => {
// fetch call used to be here
fetchData()
}, [])
return (
<div>
{
repos.map(items => console.log(items))
}
</div>
)
}
You have to verify that the repos are defined and contain data to do that you can do the following
//mock up API
const API = (ms = 800) => new Promise(resolve => setTimeout(resolve, ms, {state:200, data:[1,2,3,5]}));
ReactDOM.render(
<App />,
document.body
);
function App(props){
const [repos, setRepos] = React.useState([]);
React.useEffect(() => {
API()
.then(res => setRepos(res.data));
},[])
return <div>{
// Check Here
repos.length > 1 ? repos.map((r,i) => <div key={`${r}-${i}`}>{r}</div>) : <div>Loading ...</div>
}</div>
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
const Component = () => {
const [repos, setRepos] = useState([])
useEffect(() => {
const data = fetch('https://api.github.com/users/alexspurlock25/repos')
.then(response => response.json())
.then(data => setRepos(data))
console.log(data)
}, [])
console.log(repos)
return (
<div>
{
repos.map(items => console.log(items))
}
</div>
)
}

React async/await prop not re-rendering in child component

Link to CodeSandBox of what I am experiencing:
https://codesandbox.io/s/intelligent-chaum-eu1le6?file=/src/About.js
I am stuggling to figure out why a component will not re-render after a state changes. In this example, it is an array prop given from App.js to About.js.
a fetch request happens three times in a useEffect. Each time, it pushes it to stateArr before finally setState(stateArr)
fetch("https://catfact.ninja/fact")
.then((res) => {
return res.json();
})
.then((res) => {
stateArr.push(res);
});
fetch("https://catfact.ninja/fact")
.then((res) => {
return res.json();
})
.then((res) => {
stateArr.push(res);
});
fetch("https://catfact.ninja/fact")
.then((res) => {
return res.json();
})
.then((res) => {
stateArr.push(res);
});
setState(stateArr);
The About component is imported, and the useState variable is passed to it as a prop.
return (
<div>
<About arrayProp={state} />
</div>
);
Finally, About.js destructs the prop, and arrayProp.map() is called to render each array item on the page.
const About = ({ arrayProp }) => {
const [rerender, setRerender] = useState(0);
return (
<>
{arrayProp.map((e) => (
<div key={e.length}>
<h6>Break</h6>
{e.fact}
</div>
))}
</>
);
};
In the CodeSandBox example, I've added a button that would manually re-render the page by incrementing a number on the page.
The prop should prompt a component re-render after the fetch requests are completed, and the state is changed.
The issue is that useEffect is not behaving as described.
Each time, it pushes it to stateArr before finally setState(stateArr)
The individual fetches are not pushing to "before finally" calling setState.
const [state, setState] = useState([]);
useEffect(() => {
let stateArr = [];
function getReq() {
fetch("https://catfact.ninja/fact")
.then((res) => {
return res.json();
})
.then((res) => {
stateArr.push(res);
});
fetch("https://catfact.ninja/fact")
.then((res) => {
return res.json();
})
.then((res) => {
stateArr.push(res);
});
fetch("https://catfact.ninja/fact")
.then((res) => {
return res.json();
})
.then((res) => {
stateArr.push(res);
});
setState(stateArr);
}
getReq();
}, []);
What is actually happening is: fetch 1 is starting, then fetch 2 is starting, then fetch 3 is starting, then setState(stateArr) is being called.
There's no guarantee that these fetch will resolve before setState is called (there's similarly no guarantee that the fetches won't complete before calling setState). Though, in normal circumstances none of the fetches will resolve before setState is called.
So the only thing that's guaranteed is that state will be updated to reference the same array as stateArr. For this reason, pushing to stateArr is the same as pushing to state which is mutating state without using setState. This can cause results to be overwritten on future setState calls and it does not cause a re-render.
Well then, why does forcing re-render in About work?
As each fetch resolves it pushes values to stateArr (which is the same array as is referenced by state) for this reason the values are in the state there's just been nothing to tell React re-render (like a setState call).
Here's a small snippet which logs the promises as they complete. It also has a button that will console log the state array. (Nothing will ever render here as nothing will cause the state to update despite the state array being modified)
// Use import in normal cases; const is how use* are accessed in Stack Snippets
const {useState, useEffect} = React;
const App = () => {
const [state, setState] = useState([]);
useEffect(() => {
let stateArr = [];
function getReq() {
fetch("https://catfact.ninja/fact")
.then((res) => {
return res.json();
})
.then((res) => {
stateArr.push(res);
console.log('Promise 1 resolves', stateArr);
});
fetch("https://catfact.ninja/fact")
.then((res) => {
return res.json();
})
.then((res) => {
stateArr.push(res);
console.log('Promise 2 resolves', stateArr);
});
fetch("https://catfact.ninja/fact")
.then((res) => {
return res.json();
})
.then((res) => {
stateArr.push(res);
console.log('Promise 3 resolves', stateArr);
});
console.log('Calling Set State')
setState(stateArr);
}
getReq();
}, []);
return (
<div>
<button onClick={() => console.log(state)}>Log State Array</button>
{state.map((e) => (
<div key={e.length}>
<h6>Break</h6>
{e.fact}
</div>
))}
</div>
);
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<App/>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
<div id="root"></div>
To resolve this, simply wait for all promises to complete with Promise.all, then call setState with all the values.
const [state, setState] = useState([]);
useEffect(() => {
Promise.all([
// Promise 1
fetch("https://catfact.ninja/fact").then((res) => {
return res.json();
}),
// Promise 2
fetch("https://catfact.ninja/fact").then((res) => {
return res.json();
}),
// Promise 3
fetch("https://catfact.ninja/fact").then((res) => {
return res.json();
})
]).then((newStateArr) => {
// Wait for all promises to resolve before calling setState
setState(newStateArr);
});
}, []);
And here's a snippet demoing the result when waiting for all promises to resolve:
// Use import in normal cases; const is how use* are accessed in Stack Snippets
const {useState, useEffect} = React;
const App = () => {
const [state, setState] = useState([]);
useEffect(() => {
Promise.all([
// Promise 1
fetch("https://catfact.ninja/fact").then((res) => {
return res.json();
}),
// Promise 2
fetch("https://catfact.ninja/fact").then((res) => {
return res.json();
}),
// Promise 3
fetch("https://catfact.ninja/fact").then((res) => {
return res.json();
})
]).then((newStateArr) => {
// Wait for all promises to resolve before calling setState
setState(newStateArr);
});
}, []);
return (
<div>
{state.map((e) => (
<div key={e.length}>
<h6>Break</h6>
{e.fact}
</div>
))}
</div>
);
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<App/>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
<div id="root"></div>

React useEffect gets data from database but not in time to be used in the component

I am using useEffect to get data from an api.
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(
`/api/posts/getCats`
);
const cats = await response.json();
console.log(cats);
} catch (e) {
console.error(e);
}
};
fetchData();
}, []);
The problem is when I try to use it in the return, its value is undefined.
{cats.map((data) => {
cats has value when I console.log it.
I cannot use componentDidMount because all my code is functional components.
Edit: I updated the code as per answers below but still get
TypeError: cats.map is not a function
All answers below actually make sense but I am not sure why its not working.
export default function Posts() {
const [cats, setCats] = useState();
useEffect(() => {
fetch(`/api/posts/getCats`)
.then(res => res.json())
.then(setCats)
.catch(console.error);
}, []);
return (
<div>
{cats?.map((data) => {
<h4>{data.main}</h4>
})}
</div>
)
}
This is because React renders your screen before finishing to get response from API. When you render screen, variable cats doesn't have values. You can run useEffect after each rendering. You can rerender by changing state from useEffect (This technique is often used). Do not forget to add [] or [cats] as a dependency of useEffect (second params) otherwise you will get infinite loop.
Below code works even when cats === [] or some array.
export default () => {
const [cats, setCats] = useState([])
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(
`/api/posts/getCats`
);
const result = await response.json();
setCats(result)
} catch (e) {
}
};
fetchData();
}, []);
return (
<div>
{cats.map(cat => <div>cat</div>)}
</div>)
}
You have to map the cats data into state.
const [cats, setCats] = useState([]);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(
`/api/posts/getCats`
);
const data = await response.json();
setCats(data);
} catch (e) {
console.error(e);
}
};
fetchData();
}, []);
You need to
call setCats when the response comes back (right now, you're just logging it)
.map only once cats has been populated:
const [cats, setCats] = useState();
useEffect(() => {
fetch(`/api/posts/getCats`)
.then(res => res.json())
.then(result => setCats(result.cats))
.catch(console.error);
}, []);
return (
<div>
{cats?.map((data) => {
// ...

How to send request on click React Hooks way?

How to send http request on button click with react hooks? Or, for that matter, how to do any side effect on button click?
What i see so far is to have something "indirect" like:
export default = () => {
const [sendRequest, setSendRequest] = useState(false);
useEffect(() => {
if(sendRequest){
//send the request
setSendRequest(false);
}
},
[sendRequest]);
return (
<input type="button" disabled={sendRequest} onClick={() => setSendRequest(true)}
);
}
Is that the proper way or is there some other pattern?
export default () => {
const [isSending, setIsSending] = useState(false)
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
this is what it would boil down to when you want to send a request on click and disabling the button while it is sending
update:
#tkd_aj pointed out that this might give a warning: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."
Effectively, what happens is that the request is still processing, while in the meantime your component unmounts. It then tries to setIsSending (a setState) on an unmounted component.
export default () => {
const [isSending, setIsSending] = useState(false)
const isMounted = useRef(true)
// set isMounted to false when we unmount the component
useEffect(() => {
return () => {
isMounted.current = false
}
}, [])
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
if (isMounted.current) // only update if we are still mounted
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
You don't need an effect to send a request on button click, instead what you need is just a handler method which you can optimise using useCallback method
const App = (props) => {
//define you app state here
const fetchRequest = useCallback(() => {
// Api request here
}, [add dependent variables here]);
return (
<input type="button" disabled={sendRequest} onClick={fetchRequest}
);
}
Tracking request using variable with useEffect is not a correct pattern because you may set state to call api using useEffect, but an additional render due to some other change will cause the request to go in a loop
In functional programming, any async function should be considered as a side effect.
When dealing with side effects you need to separate the logic of starting the side effect and the logic of the result of that side effect (similar to redux saga).
Basically, the button responsibility is only triggering the side effect, and the side effect responsibility is to update the dom.
Also since react is dealing with components you need to make sure your component still mounted before any setState or after every await this depends on your own preferences.
to solve this issue we can create a custom hook useIsMounted this hook will make it easy for us to check if the component is still mounted
/**
* check if the component still mounted
*/
export const useIsMounted = () => {
const mountedRef = useRef(false);
const isMounted = useCallback(() => mountedRef.current, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
});
return isMounted;
};
Then your code should look like this
export const MyComponent = ()=> {
const isMounted = useIsMounted();
const [isDoMyAsyncThing, setIsDoMyAsyncThing] = useState(false);
// do my async thing
const doMyAsyncThing = useCallback(async () => {
// do my stuff
},[])
/**
* do my async thing effect
*/
useEffect(() => {
if (isDoMyAsyncThing) {
const effect = async () => {
await doMyAsyncThing();
if (!isMounted()) return;
setIsDoMyAsyncThing(false);
};
effect();
}
}, [isDoMyAsyncThing, isMounted, doMyAsyncThing]);
return (
<div>
<button disabled={isDoMyAsyncThing} onClick={()=> setIsDoMyAsyncThing(true)}>
Do My Thing {isDoMyAsyncThing && "Loading..."}
</button>;
</div>
)
}
Note: It's always better to separate the logic of your side effect from the logic that triggers the effect (the useEffect)
UPDATE:
Instead of all the above complexity just use useAsync and useAsyncFn from the react-use library, It's much cleaner and straightforward.
Example:
import {useAsyncFn} from 'react-use';
const Demo = ({url}) => {
const [state, doFetch] = useAsyncFn(async () => {
const response = await fetch(url);
const result = await response.text();
return result
}, [url]);
return (
<div>
{state.loading
? <div>Loading...</div>
: state.error
? <div>Error: {state.error.message}</div>
: <div>Value: {state.value}</div>
}
<button onClick={() => doFetch()}>Start loading</button>
</div>
);
};
You can fetch data as an effect of some state changing like you have done in your question, but you can also get the data directly in the click handler like you are used to in a class component.
Example
const { useState } = React;
function getData() {
return new Promise(resolve => setTimeout(() => resolve(Math.random()), 1000))
}
function App() {
const [data, setData] = useState(0)
function onClick() {
getData().then(setData)
}
return (
<div>
<button onClick={onClick}>Get data</button>
<div>{data}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
You can define the boolean in the state as you did and once you trigger the request set it to true and when you receive the response set it back to false:
const [requestSent, setRequestSent] = useState(false);
const sendRequest = () => {
setRequestSent(true);
fetch().then(() => setRequestSent(false));
};
Working example
You can create a custom hook useApi and return a function execute which when called will invoke the api (typically through some onClick).
useApi hook:
export type ApiMethod = "GET" | "POST";
export type ApiState = "idle" | "loading" | "done";
const fetcher = async (
url: string,
method: ApiMethod,
payload?: string
): Promise<any> => {
const requestHeaders = new Headers();
requestHeaders.set("Content-Type", "application/json");
console.log("fetching data...");
const res = await fetch(url, {
body: payload ? JSON.stringify(payload) : undefined,
headers: requestHeaders,
method,
});
const resobj = await res.json();
return resobj;
};
export function useApi(
url: string,
method: ApiMethod,
payload?: any
): {
apiState: ApiState;
data: unknown;
execute: () => void;
} {
const [apiState, setApiState] = useState<ApiState>("idle");
const [data, setData] = useState<unknown>(null);
const [toCallApi, setApiExecution] = useState(false);
const execute = () => {
console.log("executing now");
setApiExecution(true);
};
const fetchApi = useCallback(() => {
console.log("fetchApi called");
fetcher(url, method, payload)
.then((res) => {
const data = res.data;
setData({ ...data });
return;
})
.catch((e: Error) => {
setData(null);
console.log(e.message);
})
.finally(() => {
setApiState("done");
});
}, [method, payload, url]);
// call api
useEffect(() => {
if (toCallApi && apiState === "idle") {
console.log("calling api");
setApiState("loading");
fetchApi();
}
}, [apiState, fetchApi, toCallApi]);
return {
apiState,
data,
execute,
};
}
using useApi in some component:
const SomeComponent = () =>{
const { apiState, data, execute } = useApi(
"api/url",
"POST",
{
foo: "bar",
}
);
}
if (apiState == "done") {
console.log("execution complete",data);
}
return (
<button
onClick={() => {
execute();
}}>
Click me
</button>
);
For this you can use callback hook in ReactJS and it is the best option for this purpose as useEffect is not a correct pattern because may be you set state to make an api call using useEffect, but an additional render due to some other change will cause the request to go in a loop.
<const Component= (props) => {
//define you app state here
const getRequest = useCallback(() => {
// Api request here
}, [dependency]);
return (
<input type="button" disabled={sendRequest} onClick={getRequest}
);
}
My answer is simple, while using the useState hook the javascript doesn't enable you to pass the value if you set the state as false. It accepts the value when it is set to true. So you have to define a function with if condition if you use false in the usestate

Resources