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.
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 am trying to set up some app context that will be needed throughout the application and seemed to have run into an error that I just cant figure out. I know there are multiple questions regarding the same error, but after looking through them all I still cant seem to solve my issue. It seems as if my appContext is undefined, but I dont understand why that would be the case?
TypeError: Cannot destructure property 'setAppState' of '(0 , react__WEBPACK_IMPORTED_MODULE_0__.useContext)(...)' as it is undefined.;
Here are my files:
//appContext.js
import React, { createContext } from 'react';
const AppContext = createContext();
export default AppContext;
//appReducer.js
const AppReducer = (state, action) => {
switch (action.type) {
case 'SET_APP_STATE':
return {
...state,
openCities: [...action.payload.openCities],
};
default:
return {
...state,
};
}
};
export default AppReducer;
//appState.js
import React, { useMemo, useReducer } from 'react';
import AppContext from './appContext';
import AppReducer from './appReducer';
const AppState = (props) => {
const initialState = {
openCities: [],
};
const [state, dispatch] = useReducer(AppReducer, initialState);
async function setAppState(payload) {
dispatch({
type: 'SET_APP_STATE',
payload,
});
}
return (
<AppContext.Provider
value={{
openCities: state?.openCities,
setAppState,
}}
>
{props.children}
</AppContext.Provider>
);
};
export default AppState;
//setOpenCitiesContext.js
import { useContext, useEffect } from 'react';
import AppContext from '../context/appContext/appContext';
import getOpenCities from '../services/city/getOpenCities';
const setOpenCitiesContext = () => {
const { setAppState } = useContext(AppContext);
useEffect(() => {
fetchOpenCities();
}, []);
const fetchOpenCities = async () => {
const openCities = await getOpenCities();
setAppState(openCities.cities);
};
};
export default setOpenCitiesContext;
//app.js
import React, { useEffect } from 'react';
import TagManager from 'react-gtm-module';
import '../font/font.css';
import '../styles/globals.css';
import '../fonts/fonts.css';
import '../styles/custom.css';
import { CookiesProvider } from 'react-cookie';
import AppState from '../context/appContext/appState';
import UserState from '../context/UserState';
import Amplify from 'aws-amplify';
import awsconfig from '../public/aws-exports';
import NProgress from 'nprogress';
import Router from 'next/router';
import 'nprogress/nprogress.css';
import { GiveawayBanner } from '../components/staticStorefront/GiveawayBanner';
import setOpenCitiesContext from '../utils/setOpenCitiesContext';
Amplify.configure(awsconfig);
const tagManagerArgs = {
gtmId: 'GTM-KBTCTKD',
};
function MyApp({ Component, pageProps }) {
setOpenCitiesContext();
NProgress.configure({
minimum: 0.3,
easing: 'ease',
speed: 800,
showSpinner: false,
});
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
useEffect(() => {
TagManager.initialize(tagManagerArgs);
}, []);
return (
<CookiesProvider>
<AppState>
<UserState>
{/* <GiveawayBanner /> */}
<Component {...pageProps} />
</UserState>
</AppState>
</CookiesProvider>
);
}
For anyone that comes across this error the solution for me was to move my setOpenCitiesContext.js custom hook out of the app.js file (where my context provider was initiated) and move it into my index.js file. I am assuming having them in the same file (app.js) was causing react to call my custom hook which was trying to set the context before my context provider was defined (which as I am writing this out is exactly what the error was saying hahaha). Anyways hope my lapse in judgment can help someone else out down the line.
ps. If my thoughts above are incorrect please feel free to explain why this was happening I would love to have a deeper understanding on the issue.
I'm trying to pass data with Context API to child components. Value is getting undefined upon fetching it from a component.
Component Hierarchy:
passing data to a component MockTable and UsecasePane
MainContent -> MockTable
MainContent -> AddMock -> TabContent -> UsecasePane
=> MockContext.js
import React, { useState, useEffect, createContext } from "react";
import axios from "axios";
export const MockContext = createContext();
// provider
export const MockProvider = (props) => {
const [data, setData] = useState([]);
// data fetch and setting the state
return (
<MockContext.Provider data={[data, setData]}>
{props.children}
</MockContext.Provider>
);
};
Note: I'm getting response from the API.
Now in MainContent, components are encapsulated as follows:
// MainContent.js
import React from "react";
import { MockProvider } from "../MockContext";
const MainContent = () => {
return (
<MockProvider>
<div>
<CustomerTable />
<AddMock />
<MockTable />
</div>
</MockProvider>
);
};
When I try to fetch the data in MockTable or in UseCasePane, value is undefined.
// MockTable.js
import React, { useState, useEffect, useContext } from "react";
import { MockContext } from "./MockContext";
const MockTable = () => {
const [data, setData] = useContext(MockContext);
console.log(data);
// rest of the code
}
Please correct me where I'm going wrong :)
I tried to pass a String as well from the context and fetched in a component like:
return (
<MockContext.Provider data={"Hello"}>
{props.children}
</MockContext.Provider>
);
// in MockTable.js
const value = useContext(MockContext); ==> undefined
The correct prop to pass into the Provider is value, not data. (See: Context.Provider)
import React, { useState, useEffect, createContext } from "react";
import axios from "axios";
export const MockContext = createContext();
// provider
export const MockProvider = (props) => {
const [data, setData] = useState([]);
const fetchData = async () => {
const response = await axios
.get(config.App_URL.getAllRoute, {
params: {
customHostName: config.host,
type: config.type,
},
})
.catch((error) => {
console.error(`Error in fetching the data ${error}`);
});
console.log(response.data);
setData(response.data);
};
useEffect(() => {
fetchData();
}, []);
return (
<MockContext.Provider value={[data, setData]}>
{props.children}
</MockContext.Provider>
);
};
I have set a basic sample project that use Context to store the page title, but when I set it the component is not rerendered.
Principal files:
Context.js
import React from 'react'
const Context = React.createContext({})
export default Context
AppWrapper.js
import React from 'react'
import App from './App'
import Context from './Context'
function AppWrapper () {
return (
<Context.Provider value={{page: {}}}>
<App />
</Context.Provider>
)
}
export default AppWrapper
App.js
import React, { useContext } from 'react';
import Context from './Context';
import Home from './Home';
function App() {
const { page } = useContext(Context)
return (
<>
<h1>Title: {page.title}</h1>
<Home />
</>
);
}
export default App;
Home.js
import React, { useContext } from 'react'
import Context from './Context'
function Home () {
const { page } = useContext(Context)
page.title = 'Home'
return (
<p>Hello, World!</p>
)
}
export default Home
full code
What am I doing wrong?
Think about React context just like you would a component, if you want to update a value and show it then you need to use state. In this case your AppWrapper where you render the context provider is where you need to track state.
import React, {useContext, useState, useCallback, useEffect} from 'react'
const PageContext = React.createContext({})
function Home() {
const {setPageContext, page} = useContext(PageContext)
// essentially a componentDidMount
useEffect(() => {
if (page.title !== 'Home')
setPageContext({title: 'Home'})
}, [setPageContext])
return <p>Hello, World!</p>
}
function App() {
const {page} = useContext(PageContext)
return (
<>
<h1>Title: {page.title}</h1>
<Home />
</>
)
}
function AppWrapper() {
const [state, setState] = useState({page: {}})
const setPageContext = useCallback(
newState => {
setState({page: {...state.page, ...newState}})
},
[state, setState],
)
const getContextValue = useCallback(
() => ({setPageContext, ...state}),
[state, updateState],
)
return (
<PageContext.Provider value={getContextValue()}>
<App />
</PageContext.Provider>
)
}
Edit - Updated working solution from linked repository
I renamed a few things to be a bit more specific, I wouldn't recommend passing setState through the context as that can be confusing and conflicting with a local state in a component. Also i'm omitting chunks of code that aren't necessary to the answer, just the parts I changed
src/AppContext.js
export const updatePageContext = (values = {}) => ({ page: values })
export const updateProductsContext = (values = {}) => ({ products: values })
export const Pages = {
help: 'Help',
home: 'Home',
productsList: 'Products list',
shoppingCart: 'Cart',
}
const AppContext = React.createContext({})
export default AppContext
src/AppWrapper.js
const getDefaultState = () => {
// TODO rehydrate from persistent storage (localStorage.getItem(myLastSavedStateKey)) ?
return {
page: { title: 'Home' },
products: {},
}
}
function AppWrapper() {
const [state, setState] = useState(getDefaultState())
// here we only re-create setContext when its dependencies change ([state, setState])
const setContext = useCallback(
updates => {
setState({ ...state, ...updates })
},
[state, setState],
)
// here context value is just returning an object, but only re-creating the object when its dependencies change ([state, setContext])
const getContextValue = useCallback(
() => ({
...state,
setContext,
}),
[state, setContext],
)
return (
<Context.Provider value={getContextValue()}>
...
src/App.js
...
import AppContext, { updateProductsContext } from './AppContext'
function App() {
const [openDrawer, setOpenDrawer] = useState(false)
const classes = useStyles()
const {
page: { title },
setContext,
} = useContext(Context)
useEffect(() => {
fetch(...)
.then(...)
.then(items => {
setContext(updateProductsContext({ items }))
})
}, [])
src/components/DocumentMeta.js
this is a new component that you can use to update your page names in a declarative style reducing the code complexity/redundancy in each view
import React, { useContext, useEffect } from 'react'
import Context, { updatePageContext } from '../Context'
export default function DocumentMeta({ title }) {
const { page, setContext } = useContext(Context)
useEffect(() => {
if (page.title !== title) {
// TODO use this todo as a marker to also update the actual document title so the browser tab name changes to reflect the current view
setContext(updatePageContext({ title }))
}
}, [title, page, setContext])
return null
}
aka usage would be something like <DocumentMeta title="Whatever Title I Want Here" />
src/pages/Home.js
each view now just needs to import DocumentMeta and the Pages "enum" to update the title, instead of pulling the context in and manually doing it each time.
import { Pages } from '../Context'
import DocumentMeta from '../components/DocumentMeta'
function Home() {
return (
<>
<DocumentMeta title={Pages.home} />
<h1>WIP</h1>
</>
)
}
Note: The other pages need to replicate what the home page is doing
Remember this isn't how I would do this in a production environment, I'd write up a more generic helper to write data to your cache that can do more things in terms of performance, deep merging.. etc. But this should be a good starting point.
Here is a working version of what you need.
import React, { useState, useContext, useEffect } from "react";
import "./styles.css";
const Context = React.createContext({});
export default function AppWrapper() {
// creating a local state
const [state, setState] = useState({ page: {} });
return (
<Context.Provider value={{ state, setState }}> {/* passing state to in provider */}
<App />
</Context.Provider>
);
}
function App() {
// getting the state from Context
const { state } = useContext(Context);
return (
<>
<h1>Title: {state.page.title}</h1>
<Home />
</>
);
}
function Home() {
// getting setter function from Context
const { setState } = useContext(Context);
useEffect(() => {
setState({ page: { title: "Home" } });
}, [setState]);
return <p>Hello, World!</p>;
}
Read more on Hooks API Reference.
You may put useContext(yourContext) at wrong place.
The right position is inner the <Context.Provider>:
// Right: context value will update
<Context.Provider>
<yourComponentNeedContext />
</Context.Provider>
// Bad: context value will NOT update
<yourComponentNeedContext />
<Context.Provider>
</Context.Provider>
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.