When I write a react hook component which import '*.png' resource like this:
import React, { useState, useEffect } from 'react';
import './topMenu.less';
import intl from 'react-intl-universal';
import { Breadcrumb } from 'antd';
import home from '#/image/project/home.png';
const TopMenu: React.FC<{}> = () => {
const [page, setPage] = useState<string>('overview');
const [projectName, setProjectName] = useState<string>('');
useEffect(() => {
let hashUrl = window.location.hash;
let tempPage = hashUrl.split('/')[3];
let projectNameStr = sessionStorage.getItem('projectName');
setPage(tempPage);
setProjectName(projectNameStr ? projectNameStr : '');
}, []);
/**
* 功能描述 依据页面路由渲染面包屑组件的尾部文字
*
* #return {string} 面包屑的末级页面名称
*/
function getPageName(): string {
switch(page) {
case 'overview':
return intl.get('PROJECT_OVERVIEW');
case 'inference':
return intl.get('ROOT_CAUSE_INFERENCE');
case 'bigModel':
return intl.get('BIG_MODEL_DIAGNOSTICS');
default:
return intl.get('PROJECT_OVERVIEW');
}
}
return (<div className="slider-menu">
<Breadcrumb>
<Breadcrumb.Item href="#/kgom/packageList">
<img src={home} />
<span>{intl.get('PROJECT_LIST')}</span>
</Breadcrumb.Item>
<Breadcrumb.Item >
<span>{projectName}</span>
</Breadcrumb.Item>
<Breadcrumb.Item>
<span>{getPageName()}</span>
</Breadcrumb.Item>
</Breadcrumb>
</div>);
};
export default TopMenu;
then i write a easy test code like this:
import TopMenu from './topMenu';
import React from 'react';
import renderer from 'react-test-renderer';
jest.mock('#/image/project/home.png', () => '../image/project/home.png');
describe('TopMenu Test', () => {
it('render TopMenu', () => {
const component = renderer.create(<TopMenu />);
expect(component.toJSON()).toMatchSnapshot();
});
});
when i run npm test, it calls this error:
enter image description here
espicially this line: jest.mock('#/image/project/home.png', () => '../image/project/home.png');
I try many ways to rewrite this line, but it always error, when i not write this line, it calls that error:
enter image description here
how do i write this hook component jest render test???
Related
Error Message: React Hook "useDispatch" is called conditionally. React Hooks must be called in the exact same order in every component render
I've been trying to figure out how to fix this for days, but nothing seeems to work. The component works when I don't mock anything, but as soon as I mock dispatch it gives me this error.
Here's my component:
import { Stage } from "../Stage/Stage";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
import { retrieveStageList } from "../../modules/reducer";
import { Process } from "../Process/Process";
export function RenderProcess({
_useSelector = useSelector,
_useDispatch = useDispatch(), //this is where it breaks
_Process = Process,
}) {
const dispatch = _useDispatch();
const process = _useSelector((state) => state.renderProcess);
const stageList = _useSelector((state) => state.stageList);
useEffect(() => {
if (process.processId !== null)
dispatch(retrieveStageList(process.processId));
}, []);
return (
<>
<_Process process={process} />
{stageList?.map((stageInputs, processId) => {
return (
<div key={processId}>
<Stage stage={stageInputs} />
</div>
);
})}
</>
);
}
Here's my test for this component:
import { render } from "#testing-library/react";
import { RenderProcess } from "./RenderProcess";
test("should call dispatch once.", () => {
const _useSelector = (fn) =>
fn({
stageList: [],
renderProcess: { processId: "309624b6-9c96-4ba7-8f7e-78831614f685" },
});
const dispatch = jest.fn();
render(
<RenderProcess
_useSelector={_useSelector}
_useDispatch={() => dispatch}
_Process={() => {}}
/>
);
expect(dispatch).toHaveBeenCalledTimes(1);
});
Any help on this would be amazing.
Found the fix, in the properties being passed in RenderProcess- on this
line:
_useDispatch = useDispatch()
it should be:
_useDispatch = useDispatch
I'm in the process of building a merch e-commerce website for a client utilizing the commerce.js API however I've run into a problem. When passing the "cart" object as a prop to the checkout file it returns as an empty object which breaks the website. The web application passes the "cart" object as a prop in other parts of the code and works just fine. Is there something I'm doing wrong?
Code for reference:
import React, { useState, useEffect } from 'react';
import {Paper, Stepper, Step, StepLabel, Typography, CircularProgress, Divider, Button} from '#material-ui/core';
import { commerce } from '../../../lib/commerce';
import Addressform from '../Addressform';
import Paymentform from '../Paymentform';
const steps =['Shipping Address', 'Payment details'];
const Checkout = ({ cart }) => {
const [activeStep, setActiveStep] = useState(0);
const [checkoutToken, setCheckoutToken] = useState(null);
useEffect (() => {
const generateToken = async () => {
console.log(cart.id);
// returns as undefined
try {
const token = await commerce.checkout.generateToken(cart.id, { type: 'cart' });
console.log(token);
setCheckoutToken(token);
console.log("Success!")
} catch (error) {
console.log(error); //Returns 404 Error Obv
console.log("Didnt work")
}
}
generateToken();
}, []);
const Confirmation = () => (
<>
Confirmation
</>
);
const Form = () => activeStep === 0
? <Addressform />
: < Paymentform />
return(
<>
...
</>
);
};
export default Checkout;
I'm still new to React so forgive me if this is a silly approach to this problem.
My goal: Global error handling using a context provider and a custom hook.
The Problem: I can't remove errors without them immediately being re-added.
I display my errors via this component in the shell...
import React, { useState, useEffect } from 'react'
import Alert from '#mui/material/Alert'
import Collapse from '#mui/material/Collapse'
import { useAlertContext } from '#/context/alert-context/alert-context'
export default function AppAlert () {
const [show, setShow] = useState(false)
const alertContext = useAlertContext()
const handleClose = () => {
alertContext.remove()
setShow(false)
}
useEffect(() => {
if (alertContext.alert) {
setShow(true)
}
}, [alertContext.alert])
return (
<Collapse in={show}>
<Alert severity='error' onClose={handleClose}>
{alertContext.alert}
</Alert>
</Collapse>
)
}
I have a provider setup that also exposes a custom hook...
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
And finally I have a hook setup to hit an API and call throw errors if it any occur while fetching the data. I'm purposely triggering a 404 by passing a bad API path.
import { useEffect } from 'react'
import { useQuery } from 'react-query'
import ApiV4 from '#/services/api/v4/base'
import { useAlertContext } from '#/context/alert-context/alert-context'
export const useAccess = () => {
const alertContext = useAlertContext()
const route = '/accessx'
const query = useQuery(route, async () => await ApiV4.get(route), {
retry: 0
})
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
return query
}
This code seems to be the issue. Because alertContext.remove() triggers useEffect here and query.error still exists, it immediately re-adds the error to the page on remove. Removing alertContext from the array works, but it is not a real fix and linter yells.
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
This is a perfectly fine approach to the problem. You've also accurately identified the problem. The solution is to create a second hook with access to the methods that will modify the context. AppAlert needs access to the data in the context, and needs to update when AlertContext.alert changes. UseAccess only needs to be able to call AlertContext.add, and that method wont change and trigger a re-render. This can be done with a second Context. You can just expose one Provider and bake the actions provider into the outer context provider.
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertContextActions = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{ alert }}>
<AlertContextActions.Provider value={{ addAlert, removeAlert }}>
{children}
</AlertContextActions.Provider>
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
Now, where you need access to the alert you use one hook and where you need access to the actions you use the other.
// in AppAlert
import { useAlertContext, useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { alert } = useAlertContext()
const { removeAlert } = useAlertContextActions()
And finally
// in useAccess
import { useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { addAlert } = useAlertContextActions()
So I found a solution that seems to work for my purposes. I got a hint from this article. https://mortenbarklund.com/blog/react-architecture-provider-pattern/
Note the use of useCallback above. It ensures minimal re-renders of components using this context, as the function is guaranteed to be stable (as its memoized without dependencies).
So with this I tried the following and it solved the problem.
import React, { useState, createContext, useContext, useCallback } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = useCallback(() => setAlert(null), [])
const addAlert = useCallback((message) => setAlert(message), [])
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
My goal: Global error handling
One problem with the above useEffect approach is that every invocation of useAccess will run their own effects. So if you have useAccess twice on the page, and it fails, you will get two alerts, so it's not really "global".
I would encourage you to look into the global callbacks on the QueryCache in react-query. They are made for this exact use-case: To globally handle errors. Note that to use context, you would need to create the queryClient inside the Application, and make it "stable" with either useRef or useState:
function App() {
const alertContext = useAlertContext()
const [queryClient] = React.useState(() => new QueryClient({
queryCache: new QueryCache({
onError: (error) =>
alertContext.add(error.toString())
}),
}))
return (
<QueryClientProvider client={queryClient}>
<RestOfMyApp />
</QueryClientProvider>
)
}
I also have some examples in my blog.
I have a React app that I need to test. It's using the useContext() hook to create Provider that are using in most of my components. I have a dedicated component to handle a Context (lets say UserContext for the example) that look like that:
UserContext.jsx:
import React from 'react'
export const UserContext = React.createContext(undefined)
export const UserProvider = (props) => {
const [currentUser, setCurrentUser] = React.useState(undefined)
const context = {
currentUser,
setCurrentUser,
}
return (
<UserContext.Provider value={context}>
{props.children}
</UserContext.Provider>
)
}
So you can use the Provider like that:
import { UserProvider } from './context/UserContext'
<UserProvider>
{ ... }
</UserProvider>
Now I need to test a component that use this UserContext so let's say UserModal:
UserModal.test.jsx
import React from 'react'
import { mount } from 'enzyme'
import { BrowserRouter as Router } from 'react-router-dom'
import { UserProvider, UserContext } from '../context/UserContext'
import UserModal from '../components/UserModal'
// D A T A
import exampleUser from '../data/user.json' // Load user's data from a json file
describe('<UserModal />', () => {
let wrapper
const Wrapper = () => {
const { setCurrentUser } = React.useContext(UserContext)
React.useEffect(() => {
// Init UserContext value
setCurrentUser(exampleUser)
}, [])
return (
<UserProvider>
<UserModal />
</UserProvider>
)
}
beforeEach(() => {
wrapper = mount(<Wrapper />)
})
})
Problem is that when <UserModal /> is mounted inside of the <UserProvider>, I get an error that the currentUser in the UserContext is undefined. This error make sense because I call setCurrentUser() when the component is mounted once using React.useEffect(() => { }, []).
So have you an idea how I can mount() my <UserModal /> component inside of a context's provider in the way that the context is not undefined?
Your test should look like this:
import React from 'react'
import { mount } from 'enzyme'
import { BrowserRouter as Router } from 'react-router-dom'
import { UserProvider, UserContext } from '../context/UserContext'
import UserModal from '../components/UserModal'
// D A T A
import exampleUser from '../data/user.json' // Load user's data from a json file
describe('<UserModal />', () => {
let wrapper
const Wrapper = () => {
const { setCurrentUser } = React.useContext(UserContext)
React.useEffect(() => {
// Init UserContext value
setCurrentUser(exampleUser)
}, [])
return (
<UserModal />
)
}
beforeEach(() => {
wrapper = mount(<UserProvider><Wrapper /></UserProvider>)
})
})
Or see codesandbox here - simple test passes.
Note that UserProvider wraps Wrapper and not is used inside. It's like this because if you are using it inside, there is no UserContext to get with useContext hook, therefore there is no setCurrentUser function.
I am developing a website in which I want to be able to access the state information anywhere in the app. I have tried several ways of implementing state but I always get following error message:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
Check the render method of SOS.
Here is my SOS->index.js file:
import React, { useContext } from 'react';
import axios from 'axios';
import CONST from '../utils/Constants';
import { Grid, Box, Container } from '#material-ui/core';
import { styled } from '#material-ui/styles';
import { Header } from '../Layout';
import ListItem from './ListItem';
import SOSButton from './SOSButton';
import FormPersonType from './FormPersonType';
import FormEmergencyType from './FormEmergencyType';
import StateContext from '../App';
import Context from '../Context';
export default function SOS() {
const { componentType, setComponentType } = useContext(Context);
const timerOn = false;
//'type_of_person',
const ambulance = false;
const fire_service = false;
const police = false;
const car_service = false;
//static contextType = StateContext;
const showSettings = event => {
event.preventDefault();
};
const handleComponentType = e => {
console.log(e);
//this.setState({ componentType: 'type_of_emergency' });
setComponentType('type_of_emergency');
};
const handleEmergencyType = new_emergency_state => {
console.log(new_emergency_state);
// this.setState(new_emergency_state);
};
const onSubmit = e => {
console.log('in OnSubmit');
axios
.post(CONST.URL + 'emergency/create', {
id: 1,
data: this.state //TODO
})
.then(res => {
console.log(res);
console.log(res.data);
})
.catch(err => {
console.log(err);
});
};
let component;
if (componentType == 'type_of_person') {
component = (
<FormPersonType handleComponentType={this.handleComponentType} />
);
} else if (componentType == 'type_of_emergency') {
component = (
<FormEmergencyType
handleComponentType={this.handleComponentType}
handleEmergencyType={this.handleEmergencyType}
emergencyTypes={this.state}
timerStart={this.timerStart}
onSubmit={this.onSubmit}
/>
);
}
return (
<React.Fragment>
<Header title="Send out SOS" />
<StateContext.Provider value="type_of_person" />
<Container component="main" maxWidth="sm">
{component}
</Container>
{/*component = (
<HorizontalNonLinearStepWithError
handleComponentType={this.handleComponentType}
/>*/}
</React.Fragment>
);
}
I would really appreciate your help!
Just for reference, the Context file is defined as follows:
import React, { useState } from 'react';
export const Context = React.createContext();
const ContextProvider = props => {
const [componentType, setComponentType] = useState('');
setComponentType = 'type_of_person';
//const [storedNumber, setStoredNumber] = useState('');
//const [functionType, setFunctionType] = useState('');
return (
<Context.Provider
value={{
componentType,
setComponentType
}}
>
{props.children}
</Context.Provider>
);
};
export default ContextProvider;
EDIT: I have changed my code according to your suggestions (updated above). But now I get following error:
TypeError: Cannot read property 'componentType' of undefined
Context is not the default export from your ../Context file so you have to import it as:
import { Context } from '../Context';
Otherwise, it's trying to import your Context.Provider component.
For your file structure/naming, the proper usage is:
// Main app file (for example)
// Wraps your application in the context provider so you can access it anywhere in MyApp
import ContextProvider from '../Context'
export default () => {
return (
<ContextProvider>
<MyApp />
</ContextProvider>
)
}
// File where you want to use the context
import React, { useContext } from 'react'
import { Context } from '../Context'
export default () => {
const myCtx = useContext(Context)
return (
<div>
Got this value - { myCtx.someValue } - from context
</div>
)
}
And for godsakes...rename your Context file, provider, and everything in there to something more explicit. I got confused even writing this.