React: Context to pass state between two hierarchies of components - javascript

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.

Related

NextJS: Pass string via context from input into getStaticProps

I´m new to NextJS and React at all so I ask for your forgiveness.
I want to know how to pass an users written text from an input field (inside of Header) into the getStaticProbs function of a specific page via the react context api.
I tried the following source but it doesn`t work - it throws out an error that my way to build leads to an invalid hook call.
Here is my context source:
import React, { createContext, useState } from 'react';
export const SearchContext = createContext();
export const SearchProvider = ({ children }) => {
const [keyword, setKeyword] = useState('');
return (
<SearchContext.Provider
value={{
keyword,
setKeyword,
}}
>
{children}
</SearchContext.Provider>
);
};
to fetch the written string of SearchBar.js:
import React, { useContext, useState } from 'react';
import { useRouter } from 'next/router';
import Image from 'next/image';
import loupe from '../public/images/loupe.png';
import { SearchContext } from '../lib/searchCtx';
const SearchBar = () => {
const search = useContext(SearchContext);
const router = useRouter();
const submitAction = (e) => {
e.preventDefault();
router.push(`/searchResults`);
};
return (
<div className={styles.searchBar}>
<input
type='text'
placeholder='Suche...'
onChange={(e) => search.setKeyword(e.target.value)}
/>
<button className={styles.searchBtn} type='submit' onClick={submitAction}>
<Image src={loupe} alt='' />
</button>
</div>
);
};
export default SearchBar;
and pass it over _app.js:
import Header from '../components/Header';
import Footer from '../components/Footer';
import { SearchProvider } from '../lib/searchCtx';
function MyApp({ Component, pageProps }) {
return (
<>
<SearchProvider>
<Header />
<Component {...pageProps} />
</SearchProvider>
<Footer />
</>
);
}
}
export default MyApp;
to get the value into getStaticProbs of searchResults.js:
import { useEffect, useState, useContext } from 'react';
import { fetchData } from '../lib/utils';
import styles from '../styles/Playlist.module.scss';
import Image from 'next/image';
import { SearchContext } from '../lib/searchCtx';
export default function SearchResults({ videos }) {
console.log(videos);
const sortedVids = videos
.sort((a, b) =>
Number(
new Date(b.snippet.videoPublishedAt) -
Number(new Date(a.snippet.videoPublishedAt))
)
)
return (
<>
<div className={`${styles.playlist_container} ${styles.search}`}>
<h1>Search results</h1>
{sortedVids
.map((vid, id) => {
return (
<div className={styles.clip_container}>
<Image
className={styles.thumbnails}
src={vid.snippet.thumbnails.medium.url}
layout='fill'
objectFit='cover'
alt={vid.snippet.title}
/>
</div>
<div className={styles.details_container}>
<h3>{vid.snippet.title}</h3>
</div>
);
})}
</div>
</>
);
}
export async function getStaticProps() {
const search = useContext(SearchContext);
const { YOUTUBE_KEY } = process.env;
const uploadsURL = `https://youtube.googleapis.com/youtube/v3/search?part=snippet&channelId=UCbqKKcML7P4b4BDhaqdh_DA&maxResults=50&key=${YOUTUBE_KEY}&q=${search.keyword}`;
async function getData() {
const uploadsData = fetchData(uploadsURL);
return {
videos: await uploadsData,
};
}
const { videos } = await getData();
return {
revalidate: 86400,
props: {
videos: videos.items,
},
};
}
Would you help me by 1) telling me the main failure I did and 2) providing me a working source?
How can I achieve it to get the keyword from SearchContext into the uploadsURL (inside of getStaticProbs) or isn`t it possible?
Thanks in advance!!
You can create a dynamic pages under your page folder one called say index.js and one called [slug].js (all under one folder) In the index page you can have your normal search input, when the users submit the query you can do
<a
onClick={() =>
router
.push(`/movies/${search.keyword}`)
.then(() => window.scrollTo(0, 0))}>
search
</a>
and in your [slug].js page you can retrieve that information like so
export async function getServerSideProps(pageContext) {
const pageQuery = pageContext.query.slug;
const apiCall= await fetch(
``https://youtube.googleapis.com/youtube/v3/search?part=snippet&channelId=UCbqKKcML7P4b4BDhaqdh_DA&maxResults=50&key=${YOUTUBE_KEY}&q=${pageQuery}`
);
const results = await apiCall.json();
return {
props: {
data: results,
},
};
}
I don't know if this will work for you but is a solution

Eslint: component definition is missing display name for Combined Context

I have a function that combines multiple context provider components and returns one combined Context Provider. I am getting an eslint error: Component definition is missing display name (eslintreact/display-name). How can I resolve the error?
const providers = [AuthProvider, CartProvider, LocationProvider];
const combineComponents = (...components: FC[]): FC => {
return components.reduce(
(AccumulatedComponents, CurrentComponent) => {
return ({ children }: ComponentProps<FC>): JSX.Element => {
return (
<AccumulatedComponents>
<CurrentComponent>{children}</CurrentComponent>
</AccumulatedComponents>
);
};
},
({ children }) => <>{children}</>
);
};
export const CombinedContextProvider = combineComponents(...providers);
The CombinedContextProvider component can then be used in App.tsx in the following way:
import React from 'react';
const App: React.FC = ({ Component, pageProps }) => {
<CombinedContextProvider>
<Component {...pageProps} />
</CombinedContextProvider>
)};
export default App;
Based on a comment that mentioned the issue was an anonymous function, I was able to resolve the error with the following code:
import React, { ComponentProps, FC } from 'react';
import { AuthProvider } from './auth_context';
import { CartProvider } from './cart_context';
import { LocationProvider } from './location_context';
const providers = [AuthProvider, CartProvider, LocationProvider];
const combineComponents = (...components: FC[]): FC => {
return components.reduce(
(AccumulatedComponents, CurrentComponent) => {
return function AccumulateComponents({ children }: ComponentProps<FC>): JSX.Element {
return (
<AccumulatedComponents>
<CurrentComponent>{children}</CurrentComponent>
</AccumulatedComponents>
);
};
},
({ children }) => <>{children}</>,
);
};
export const CombinedContextProvider = combineComponents(...providers);

How to use Context Api in Next.js

First I would like to say to I've never worked with either next.js or the context api so please bear with me.
I'm currently working on a web application in Next.js where I have multiple pages that each contain a form. I would like to have a global state of some sort in order to be able to set and update the data from each form. All form data together
For example: page 1 = name, page 2 = description, ...
From what I've read online, I thought that using the context api would be sufficient, but I've hit a wall. When I fill in the name on the first form it doesn't get saved in the global state because it doesn't show up on the second page.
I don't understand where I went wrong so any help i more than welcome!
p.s. if i didn't explain some part right or forgot to add some code snippet please let me know.
businessContext.tsx
import { createContext, useState } from "react";
//accessible data
export interface BusinessContextData {
businessName: string;
handleBusinessName: (name: string) => void;
}
//default values
export const businessContextDefaultValue: BusinessContextData = {
businessName: "",
};
//provider
export const BusinessContext = createContext<BusinessContextData>(
businessContextDefaultValue
);
//hooks that components can use to change the values
export function useBusinessContextValue(): BusinessContextData {
const [businessName, setBName] = useState<string>("");
const handleBusinessName = (name: string) => {
setBName(name);
};
return {
businessName,
handleBusinessName,
};
}
_app.tsx
import type { AppProps } from "next/app";
import {
useBusinessContextValue,
BusinessContext,
} from "../context/businessContext";
import "../styles/global.css";
function MyApp({ Component, pageProps }: AppProps) {
const businessContextValue = useBusinessContextValue();
return (
<BusinessContext.Provider value={businessContextValue}>
<Component {...pageProps} />
</BusinessContext.Provider>
);
}
export default MyApp;
businessName.tsx - name form page (should save the given name in global state)
import { ChangeEvent, FormEvent, useContext, useState } from "react";
import { BusinessContext } from "../context/businessContext";
const IndexPage = () => {
const { handleBusinessName } = useContext(BusinessContext);
const router = useRouter();
const [businessNameState, setBusinessnameState] = useState<string>("");
const onSubmit = (e: FormEvent) => {
handleBusinessName(businessNameState);
router.push("/businessVision");
};
return (
...
<form onSubmit={(e: FormEvent) => onSubmit(e)}>
<div className="formInputRow">
<input
className="formInput"
type="text"
placeholder="Business name"
required
value={businessNameState}
onChange={(val: ChangeEvent<HTMLInputElement>) =>
setBusinessnameState(val.target.value)
}
/>
</div>
<button type="submit">
Next
</button>
</form>
...
);
};
export default IndexPage;
businessVision.tsx - should display business name from global state
import { ChangeEvent, FormEvent, useContext, useEffect, useState } from "react";
import { BusinessContext } from "../context/businessContext";
const BusinessVisionpage = () => {
const { businessName } = useContext(BusinessContext);
const router = useRouter();
const [businessVisionState, setBusinessVisionState] = useState<string>("");
return (
...
<h1>
<span>{businessName}</span>
</h1>
...
);
};
Your context is setup correctly, but you'll need to prevent the <form>'s submit default behaviour so that the navigation to the next page can happen properly.
const onSubmit = (e: FormEvent) => {
e.preventDefault(); // Prevents submit default behaviour
handleBusinessName(businessNameState);
router.push("/businessVision");
};

Enzime: Wait that context is updated to start execute tests

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.

React context not updating

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>

Categories

Resources