Test child components based on the results from an async function - javascript

I have a parent component that renders a list of child components, but it's based on the result I got from an API call, set the state of the parent component, and render the child components using this.state. The problem I have right now is that since setState is an async function, by the time the parent component is mounted using Enzyme, the setState hasn't completed yet and therefore the child components don't exist in the DOM. I just want to know what is the correct way to test the existence of child components in this case?
Any help would be appreciated!
ParentComponent.js
componentDidMount() {
someAsyncAPICall().then(status => {
this.setState({status: status});
});
}
render() {
return (
{this.state.status.map((object, rowIndex) =>
<ChildComponent key={object.id}
...
/>
)}
);
}
Mocha unit test for parent
import React from 'react';
import {expect} from 'chai';
import {mount} from 'enzyme';
import fetchMock from 'fetch-mock';
...
describe('ParentComponent', () => {
const baseURL = "http://localhost:8000";
const status = [{
...
}];
before(() => {
fetchMock.get(`${baseURL}/api`, status);
});
after(() => {
fetchMock.reset();
fetchMock.restore();
});
it('contains the correct number of components', (done) => {
const wrapper = mount(<ParentComponent />);
//This is not working - the length of ChildComponent is always 0
expect(wrapper.find(ChildComponent).length).to.equal(status.length);
});
});

Related

Props alternative to pass the component state to all the components of application in next.js

I want to pass the setState method of the component (SnackBar) to all the child components of the _app.js. If I pass the setState method of SnackBar to all the child components of _app.js then it will be a very tedious task. Because, there are approx 4 levels of hierarchy from _app.js to the single component node. It includes,
_app.js -> pages -> layouts -> sections -> components
The snippet of _app.js is here.
function MyApp({ Component, pageProps }) {
const [ toastOpen, setToastOpen ] = React.useState({
msg: '',
open: false
});
React.useEffect(() => {
pageProps = { ...pageProps, setToastOpen };
}, []);
return (
<React.Fragment>
<ToastMessage
message={ toastOpen.msg }
setOpenState={ setToastOpen }
openState={ toastOpen.open }
/>
<Component {...pageProps} />
</React.Fragment>
)
}
Is there any way that I can directly import the setToastOpen method in the child component and use it whenever I need it?
React have a special Feature called Context Api , using that you can skip the props chain passed into your components..
I recomend you to checkout below resources to learn about context Api -
https://reactjs.org/docs/context.html
https://www.freecodecamp.org/news/react-context-in-5-minutes
Example of ContextAPI
Create a seperate file for Context Toast-context.js , You can use any name you want.
import React, { useState } from "react"
const ToastContext = React.createContext({
message: "",
toastOpen: false,
toggleToast: () => { },
changeMessage: () => { }
})
export const ToastContextProvider = ({children}) => {
/*you need to use
this component to wrap App.js so that the App.js and all of its children
and their children components and so on will get the access to the
context*/
const [toastOpen, setToastOpen] = useState(false);
const [message, setMessage] = useState("");
const toggleToast = () => {
setToastOpen(true)
}
const changeMessage = (message) => {
setMessage(message);
}
return (
<ToastContext.Provider value={
toastOpen,
message,
toggleToast,
changeMessage
}>
{children}
</ToastContext.Provider>
)
}
now in the App.js file you need to wrap your components with ToastContextProvider component
import React, { useContext } from "react";
import { ToastContextProvider } from "./Toast-context";
import { ToastContext } from "./Toast-context";
function MyApp({ Component, pageProps }) {
const { message, toastOpen, toggleToast, changeMessage } =
useContext(ToastContext);
return (
<ToastContextProvider>
{toastOpen && <div className="toast">{message}</div>}
</ToastContextProvider>
);
}
just import the context using useContext Hook in any component you want. you don't need to wrap with <ToastContextProvider> in every component.
just use useContext hook and then you can see the state and as well as call the functions methods to change the state.
Also make sure to refer the above links to learn more about Context Api. Thank You

Cannot update a component (`App`) error in component

I have the following component:
import React, { useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import DashboardContext from '../../contexts/DashboardContext';
import authorizeWorker from '../../workers/authorize-worker';
/**
* A protected route that calls the authorize API and redirects to login
* on fail
* #param {Function} component The component to redirect to on success
*/
const ProtectedRoute = ({ component }) => {
const [isAuthenticated, setIsAuthenticated] = useState(null);
const dashboardContext = useContext(DashboardContext);
dashboardContext.setIsDashboard(true);
const Component = component;
useEffect(() => {
authorizeWorker({})
.then(() => setIsAuthenticated(true))
.catch(() => setIsAuthenticated(false));
}, []);
if (isAuthenticated === true) {
return <Component />;
}
if (isAuthenticated === false) {
return <Redirect to="/login" />;
}
return null;
}
ProtectedRoute.propTypes = {
component: PropTypes.func
};
export default ProtectedRoute;
I use this for my router e.g.
<ProtectedRoute path="/projects" component={Projects} />
I am recently seeing a warning in the console: Warning: Cannot update a component (App) while rendering a different component (ProtectedRoute). To locate the bad setState() call insideProtectedRoute, follow the stack trace as described. Why am I seeing this error and how can I fix it?
The error is because on initial render phase, you render the component with setIsDashboard(true);, usually, you want to do it on mount (useEffect with empty dep array).
There is an initial render phase, then mount phase, see component's lifecycle diagram.
Be sure that setIsDashboard is persistent, meaning it is created by React API (like useState).
Or its memoized with useMemo/useCallback, or you will get inifite loop because on every render a new instance of setIsDashboard will be created and the dep array ([setIsDashboard]) will cause another execution.
const ProtectedRoute = ({ component }) => {
const { setIsDashboard } = useContext(DashboardContext);
// Make sure `setIsDashboard` persistant
useEffect(() => {
setIsDashboard(true);
}, [setIsDashboard]);
...
};
The error is because you can't set state when you are rendering:
dashboardContext.setIsDashboard(true); is probably the problem.
You don't post your stack trace or line numbers so it's hard to tell exactly what the issue is:
https://stackoverflow.com/help/minimal-reproducible-example

Render methods should be a pure function of props and state

I am receiving the error of Warning: Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state..
It appears that it is because of this code
const LoginAuth = () => {
navigate(routes.INDEX);
return null;
};
removing navigate(routes.INDEX); stops the error.
What is wrong with the code? Should I be using another method to redirect an authUser? Any help is appreciated.
It is a part of
import React from 'react';
import { navigate } from 'gatsby';
import AuthUserContext from './AuthUserContext';
import withAuthentication from './withAuthentication';
import Layout from './Layout';
import LoginForm from './LoginForm';
import * as routes from '../../constants/routes';
const LoginAuth = () => {
navigate(routes.INDEX);
return null;
};
const LoginPage = () => (
<Layout>
<Transition>
<AuthUserContext.Consumer>
{authUser => (authUser ? <LoginAuth /> : <LoginNonAuth />)}
</AuthUserContext.Consumer>
</Transition>
</Layout>
);
const LoginNonAuth = () => <LoginForm />;
export default withAuthentication(LoginPage);
Stateless functional components are expected to be pure functions, i.e. contain no side effects, while navigate() provides side effects.
Side effects are supposed to be applied after the component is mounted, that's the purpose of componentDidMount hook.
It should be:
class LoginAuth extends Component {
componentDidMount() {
navigate(routes.INDEX);
}
render() {
return null;
}
}
With the introduction of stateful functional components, side effects belong to useEffect hook:
const LoginAuth = () => {
useEffect(() => {
navigate(routes.INDEX);
}, []);
return null;
};

How to test the re-rendering triggered by setState() in componentDidMount() in React 16

I have a component that trigger re-rendering when I call setState() in componentDidMount(). Here is sample code:
import React from 'react'
class MyComponent extends React.Component {
constructor (props) {
super(props)
this.state = {
message: 'Hello'
}
}
componentDidMount () {
this.fetchMessage().then((message) => {
this.setState({ message })
})
}
fetchMessage() {
return Promise.resolve('World')
}
render() {
return this.state.message
}
}
and my test is like this:
import MyComponent from './MyComponent'
import { shallow } from 'enzyme'
describe('<MyComponent>', () => {
it('renders World', () => {
const wrapper = shallow(<MyComponent />)
expect(wrapper.text()).toEqual('World') # => results shows Hello
})
})
Please note, I am using enzyme to assist with my test.
The test failed because the returned results is Hello which is the initial rendering.
So my question here is what's the way to ensure that the re-rendering has triggered before carrying out the assertion?
Hellow,
You have to use mount if you want to test lifeCycleMethod.
Note that setState is async, so if this.fetchMessage take time, you will find yourself into a beautiful race condition on your render and in your test.
To avoid this, try to extract the render into a stateless function.

React: How mock functions and test component rendering with jest:

Im very new to react/jest. Im trying to test a very simple react component that gets data from the server and renders the response. My component looks like the below:
export default class myComponent extends Component {
constructor(props) {
super(props);
}
async componentDidMount() {
try {
let response = await axios.get(`server/url/endpoint`);
this._processSuccess(response.data);
} catch(e) {
this._processFail(e);
}
}
_processSuccess(response) {
this.setState({pageTitle: response.data.title, text: response.data.text});
}
render() {
return (
<div className="title">{this.state.pageTitle}</div>
);
}
}
Now I want to test this class. While I test:
I want to make sure componentDidMount() was not called
I want to pass test data to _processSuccess
Finally check the if the rendered output contains a div with class title that has the inner text same as what I supplied as response.data/pageTitle
I tried something like the below:
import React from 'react'
import MyComponent from './MyComponent'
import renderer from 'react-test-renderer'
import { shallow, mount } from 'enzyme'
describe('MyComponent', () => {
it('should display proper title', () => {
const c = shallow(<MyComponent />);
c._processSuccess(
{data:{pageTitle:'siteName', test:'text'}}
);
// couldn't go further as Im getting error from the above line
});
});
But, Im getting MyComponent._processSuccess is not a function error. What would be the proper way to do that.
shallow() returns an Enzyme wrapper with some utils method to test the rendered component. It does not return the component instance. That's why you get the error when calling c._processSucces(). To access the component you can use the .instance() method on the wrapper, so the following should work:
const c = shallow(<MyComponent />);
c.instance()._processSuccess(
{data:{pageTitle:'siteName', test:'text'}}
);
In order to avoid that component's componentDidMount() get called, you can try settings disableLifecycleMethods on the shallow renderer, but I'm not sure about that because here Enzyme's documentation is not 100% clear:
const c = shallow(<MyComponent />, {
disableLifecycleMethods: true
});
Finally, you can check if the output contains the expected <div>, using Enzyme's contains() and one of Jest assertion methods:
expect(c.contains(<div className="title" />)).toBe(true);

Categories