component which I'm exporting and want to test:
export default connectToStore(DefaultComponent);
connectToStore wrapper around component:
import React from 'react';
import AppContext from '../components/context/AppContext';
const connectToStore = Component => props => (
<AppContext.Consumer>
{({ state }) => (
<Component {...props} state={state} />
)}
</AppContext.Consumer>
);
export default connectToStore;
unit test calling component
it('should render view', () => {
const wrapper = render(<DefaultComponent />);
expect(wrapper.html()).toBeTruthy();
});
Error which I get:
Cannot destructure property state of 'undefined' or 'null'.
How do you test a component in general if it has a wrapper around it when being exported? How can I have the state injected hence being present in the wrapper.
Just to offer an alternative that's less complicated from a test perspective, you could have simply included a named export for the component itself as a test harness
export { DefaultComponent }
export default connectToStore(DefaultComponent)
That way your original test would still stand, you would just need to import as
import { DefaultComponent } from './defaultComponent'
And the of course when mounting mock the state prop provided by your context
const wrapper = render(<DefaultComponent state={{ ... }} />);
AppContext value property needs to be mocked:
Solution:
const wrapper = mount(
<AppContext.Provider
value={{
data,
callbackList: {}
}}
>
<DefaultComponent />
</AppContext.Provider>
);
Related
My React app has the following in App.js:
const App = () => (
<Router>
<Switch>
... various routes, all working fine ...
<Route exact path={ROUTES.DASHBOARD} render={(props) => <Dashboard {...props} />}/>
</Switch>
</Router>
);
I'm getting an error on Dashboard, which says JSX element type 'Dashboard' does not have any construct or call signatures.
This is because Dashboard is created like this:
const DashboardPage = ({firebase}:DashboardProps) => {
return (
<div className="mainRoot dashboard">...contents of dashboard...</div>
);
}
const Dashboard = withFirebase(DashboardPage);
export default Dashboard;
and withFirebase is:
import FirebaseContext from './firebaseContext';
const withFirebase = (Component:any) => (props:any) => (
<FirebaseContext.Consumer>
{firebase => <Component {...props} firebase={firebase} />}
</FirebaseContext.Consumer>
);
export default withFirebase;
So withFirebase is exporting a JSX element, so that's what Dashboard is. How can I ensure that withFirebase is exporting a Component instead?
So withFirebase is exporting a JSX element, so that's what Dashboard is. How can I ensure that withFirebase is exporting a Component instead?
withFirebase is not creating a JSX element, it is creating a function which creates a JSX Element -- in other words that's a function component. Perhaps it helps to type it properly.
const withFirebase = <Props extends {}>(
Component: React.ComponentType<Omit<Props, "firebase"> & { firebase: Firebase | null }>
): React.FC<Props> => (props) => (
<FirebaseContext.Consumer>
{(firebase) => <Component {...props} firebase={firebase} />}
</FirebaseContext.Consumer>
);
Those type are explained in detail in this answer. Is your context value sometimes null? Can your DashboardPage handle that, or do we need to handle it here? Here's one way to make sure that DashboardPage can only be called with a valid Firebase prop.
const withFirebase = <Props extends {}>(
Component: React.ComponentType<Omit<Props, "firebase"> & { firebase: Firebase }>
): React.FC<Props> => (props) => (
<FirebaseContext.Consumer>
{(firebase) =>
firebase ? (
<Component {...props} firebase={firebase} />
) : (
<div>Error Loading Firebase App</div>
)
}
</FirebaseContext.Consumer>
);
Now that we have fixed the HOC, your Dashboard component has type React.FC<{}>. It's a function component that does not take any props.
You do not need to create an inline render method for your Route (this will actually give errors about incompatible props). You can set it as the component property component={Dashboard}.
complete code:
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
// placeholder
class Firebase {
app: string;
constructor() {
this.app = "I'm an app";
}
}
const FirebaseContext = React.createContext<Firebase | null>(null);
const withFirebase = <Props extends {}>(
Component: React.ComponentType<Omit<Props, "firebase"> & { firebase: Firebase }>
): React.FC<Props> => (props) => (
<FirebaseContext.Consumer>
{(firebase) =>
firebase ? (
<Component {...props} firebase={firebase} />
) : (
<div>Error Loading Firebase App</div>
)
}
</FirebaseContext.Consumer>
);
interface DashboardProps {
firebase: Firebase;
}
const DashboardPage = ({ firebase }: DashboardProps) => {
console.log(firebase);
return <div className="mainRoot dashboard">...contents of dashboard...</div>;
};
const Dashboard = withFirebase(DashboardPage);
const App = () => {
const firebase = new Firebase();
return (
<FirebaseContext.Provider value={firebase}>
<Router>
<Switch>
<Route component={Dashboard} />
</Switch>
</Router>
</FirebaseContext.Provider>
);
};
export default App;
I'm playing with React Context API. I created a simple component:
import React, { createContext, useContext } from 'react';
const MyContext = createContext(1);
const MyComponent = () => (
<>
<p>{useContext(MyContext)}</p>
<MyContext.Provider value={2}>
<p>{useContext(MyContext)}</p>
</MyContext.Provider>
</>
);
export default MyComponent;
I'm getting two <p>1</p>. Why isn't the second context updated with 2? Am I using useContext() incorrectly?
You must use a separate Component to get Context to work.
I've filed a bug about this; see https://github.com/facebook/react/issues/18629
Simply split the code using the Context into a different Component.
const Inner = () => (
<p>{useContext(MyContext)}</p>
);
const MyComponent = () => (
<>
<p>{useContext(MyContext)}</p>
<MyContext.Provider value={2}>
<Inner />
</MyContext.Provider>
</>
);
That should fix it.
You'll need to render another component inside the context provider to get the value of 2. As useContext's documentation states:
Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest <MyContext.Provider> above the calling component in the tree.
Emphasis added. The important point is that it doesn't matter where you call useContext inside the component - what matters is where that component where it's called in is located in the tree.
import React, { createContext, useContext } from "react";
const MyContext = createContext(1);
const ChildComponent = () => (
<p>{useContext(MyContext)}</p>
)
const MyComponent = () => (
<>
<p>{useContext(MyContext)}</p>
<MyContext.Provider value={2}>
<ChildComponent/>
</MyContext.Provider>
</>
);
export default MyComponent;
I am currently reworking my DataProvider, updating it from a class component to a functional component with React Hooks.
I believe my issue is in the way I am setting up my context consumer but I haven't found a good way to test this.
DataProvider.js
import React, { createContext } from "react";
const DataContext = createContext();
export const DataProvider = (props) => {
const [test, setTest] = React.useState("Hello");
return (
<DataContext.Provider value={test}>{props.children}</DataContext.Provider>
);
};
export const withContext = (Component) => {
return function DataContextComponent(props) {
return (
<DataContext.Consumer>
{(globalState) => <Component {...globalState} {...props} />}
</DataContext.Consumer>
);
};
};
So my withContext function should receive a component and pass it the props of the Context Provider.
I try to pull in my test state into a component.
import React from "react";
import style from "./DesktopAboutUs.module.css";
import { withContext } from "../../DataProvider";
const DesktopAboutUs = ({ test }) => {
return (
<div className={style.app}>
<div>{test}</div>
</div>
);
};
export default withContext(DesktopAboutUs);
No data is showing up for test. To me this indicates that my withContext function is not properly receiving props from the Provider.
Because you passed value={test}, globalState is a string, not an object with a test property.
Either of these solutions will result in what you expected:
Pass an object to the value prop of DataContext.Provider using value={{ test }} instead of value={test} if you intend globalState to contain multiple props.
Pass globalState to the test prop of Component using test={globalState} instead of {...globalState} if you do not intend globalState to contain multiple props.
const DataContext = React.createContext();
const DataProvider = (props) => {
const [test, setTest] = React.useState("Hello");
return (
<DataContext.Provider value={{ test }}>
{props.children}
</DataContext.Provider>
);
};
const withContext = (Component) => (props) => (
<DataContext.Consumer>
{(globalState) => <Component {...globalState} {...props} />}
</DataContext.Consumer>
);
const DesktopAboutUs = ({ test }) => (
<div>{test}</div>
);
const DesktopAboutUsWithContext = withContext(DesktopAboutUs);
ReactDOM.render(
<DataProvider>
<DesktopAboutUsWithContext />
</DataProvider>,
document.querySelector('main')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<main></main>
I'm using a template for my React app, and I came across one component that constantly gives me the following warning:
Warning: componentWillMount has been renamed, and is not recommended for use. See https:... for details.
Now, I'm trying to rewrite this component to React Hooks, to avoid multiple warnings in the console.
This is how the component looks like:
import React, { Component } from 'react';
import Nprogress from 'nprogress';
import ReactPlaceholder from 'react-placeholder';
import 'nprogress/nprogress.css';
import 'react-placeholder/lib/reactPlaceholder.css';
import CircularProgress from '../components/CircularProgress/index';
export default function asyncComponent(importComponent) {
class AsyncFunc extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
};
}
componentWillMount() {
Nprogress.start();
}
componentWillUnmount() {
this.mounted = false;
}
async componentDidMount() {
this.mounted = true;
const { default: Component } = await importComponent();
Nprogress.done();
if (this.mounted) {
this.setState({
component: <Component {...this.props} />,
});
}
}
render() {
const Component = this.state.component
|| (
<div
className="loader-view"
style={{ height: 'calc(100vh - 200px)' }}
>
<CircularProgress />
</div>
);
return (
<ReactPlaceholder type="text" rows={7} ready={Component !== null}>
{Component}
</ReactPlaceholder>
);
}
}
return AsyncFunc;
}
And here is the example of its usage:
import React from 'react';
import { Route, Switch, withRouter } from 'react-router-dom';
import asyncComponent from '../util/asyncComponent';
const Routes = ({ match }) => (
<Switch>
<Route
path={`${match.url}/main`}
component={asyncComponent(() => import('./routes/MainPage/index'))}
/>
{/* <Route component={asyncComponent(() => import("app/routes/extraPages/routes/404"))}/> */}
</Switch>
);
export default withRouter(Routes);
I know how to rewrite component lifecycle methods (componentDidMount, componentWillUnmount should be rewritten to useEffect), but I don't understand part with props - asyncComponent gets importComponent as a prop, but where are we getting props in AsyncFunc? And how this could be rewritten to the functional component?
As I understood, asyncComponent is a Higher Order Component that responds with an updated component.
Sorry for not providing a sandbox for this example.
I couldn't test this code but I think is a solution
import React, { useState, useEffect } from 'react';
import Nprogress from 'nprogress';
import ReactPlaceholder from 'react-placeholder';
import 'nprogress/nprogress.css';
import 'react-placeholder/lib/reactPlaceholder.css';
import CircularProgress from '../components/CircularProgress/index';
const asyncComponent = importComponent => {
const [component, setComponent] = useState(null);
Nprogress.start();
useEffect(async () => {
const { default: Component } = await importComponent();
Nprogress.done();
setComponent(<Component {...importComponent} />);
}, []);
return component ? (
<ReactPlaceholder type="text" rows={7} ready>
{component}
</ReactPlaceholder>
) : (
<div className="loader-view" style={{ height: 'calc(100vh - 200px)' }}>
<CircularProgress />
</div>
);
};
export default asyncComponent;
I don't see the need to use the state mounted because you only use it in the dismount to setState component, but if 2 lines before you set mounted as true, it is not necessary to generate a re-render, you can go and setState component directly.
I hope this helps you.
According to reactjs.org, componentWillMount will not be supported in the future. https://reactjs.org/docs/react-component.html#unsafe_componentwillmount
There is no need to use componentWillMount.
Before discussing your question but where are we getting props in AsyncFunc?
start with an example
const A = props => {
return <p>{JSON.stringify(props.location)}</p>;
};
function App() {
return (
<Switch>
<Route exact path="/" component={A} />
<Route exact path="/a" component={p => <A {...p} />} />
</Switch>
);
}
Route / and route /a both components are getting props but in different ways
on route / the props are automatically passed to the component
As you know HOC take a component and in response, it returns a component
so asyncComponent(() => import('./routes/MainPage/index') will return AsyncFunc
so we can simply say that
<Route
path={`${match.url}/main`}
component={AsyncFunc}
/>
and that's all
component={AsyncFunc} is equal to component={(p) => <AsyncFunc {...p} />}
and that how pros are passing
Every time I run yarn start with the following line of code const context = useContext(GlobalContext); I run into this "Error: Invalid hook call. Hooks can only be called inside of the body of a function component." How can I fix this it's driving me mental. here's a screenshot showing the error, the dependencies and my code
here is the code for my globalState if that's whats causing it
import React, { createContext, useReducer } from "react";
import AppReducer from './AppReducer';
// initial state
const initialState = {
healthData:
{ age: "38", gender: "male", goal: "Lose", height: "180.34", weight: 80 }
}
export const GlobalContext = createContext(initialState);
// provider component
export const GlobalProvider = ({ children }) => {
const [state, dispatch] = useReducer(AppReducer, initialState);
return (<GlobalContext.Provider value={{
healthData: state.healthData
}}>
{children}
</GlobalContext.Provider>);
}
export default (state, action) => {
switch(action.type) {
default:
return state;
}
}
return (
<div>
<Router>
<div className='App'>
{this.state.user ? (<Nav drawerClickHandler={this.drawerToggleClickHandler} />) : (<Login_bar />)}
<SideDrawer sidedrawerClickHandler={this.sidedrawerToggleClickHandler} show={this.state.sideDrawerOpen} />
{backdrop}
<GlobalProvider>
{this.state.user ?
(< Switch >
<Route path='/settings_page' component={settings_page} exact />,
<Route path='/setup_page' component={setup_page} exact />,
<Route path='/' component={Main_page} />
</Switch>) : (<Login_page />)}
</GlobalProvider>
</div>
</Router>
</div >
);
So I've done some research into this and apparently it's likely that either my react or react-dom are not up to date. Iv tried updating both multiple time and running npm install. didn't fix my issue, I'm also almost certain that I'm calling on hooks correctly. Another thing is my global state is set up correctly because I can see it in the react chrome extension.
Thats because Hooks can not be used in a class based component. Alternatively you can create a HOC component an use it in your class based component, or covert your class component into functional component.