Invalid hook call when setting up context with a reducer - javascript

I'm trying to setup an AppProvider component that uses a context and reducer to handle global data for my app. However it looks like the useReducer hook that is called within AppProvider is causing an error. I've read the link in the error message and as far as I can tell I'm not violating any of the rules listed there.
import { merge } from 'lodash';
import React, { useReducer, Reducer, useContext, Dispatch } from 'react';
export interface AppState {
stuff: string;
}
const initialState: AppState = { stuff: 'something' };
const AppStateContext = React.createContext(initialState);
export type Action = { type: 'SET_STATE'; state: Partial<AppState> };
const AppDispatchContext = React.createContext<Dispatch<Action>>(
() => initialState,
);
const reducer: Reducer<AppState, Action> = (
state: AppState,
action: Action,
): AppState => {
switch (action.type) {
case 'SET_STATE':
return { ...merge(state, action.state) };
default:
return { ...state };
}
};
// eslint-disable-next-line #typescript-eslint/no-explicit-any
export type AppProviderProps = AppState & { children: any };
export const AppProvider = ({ children, ...rest }: AppProviderProps) => {
const [state, dispatch] = useReducer(reducer, {
...initialState,
...rest,
});
return (
<AppDispatchContext.Provider value={dispatch}>
<AppStateContext.Provider value={state}>
{children}
</AppStateContext.Provider>
</AppDispatchContext.Provider>
);
};
// Hooks
export const useAppContextState = (): AppState => useContext(AppStateContext);
export const useAppContextDispatch = (): React.Dispatch<Action> =>
useContext(AppDispatchContext);
I'm using it with a new app created with create-react-app:
import queryString from 'query-string';
import React from 'react';
import { AppProvider } from './app/AppProvider';
import logo from './logo.svg';
import './App.css';
function App() {
const { stuff } = queryString.parse(document.location.search);
if (typeof stuff !== 'string') {
return (
<div>
Missing stuff parameter.
</div>
);
}
return (
<AppProvider stuff={stuff}>
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
</AppProvider>
);
}
export default App;
The error thrown:
Package versions:
react#17.0.2 (the only version installed)
react-dom#17.0.2
react-scripts#4.0.3

The issue was in fact caused by having two different react packages installed. The app was part of a monorepo and yarn hoisted the react-dom package. The fix was to simply prevent hoisting react and react-dom:
In package.json:
{
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/react",
"**/react/**",
"**/react-dom",
"**/react-dom/**",
]
}
}

Related

Getting an error that I need to use middleware, but I have already applied middleware

Okay so I am following a tutorial, and I'm a beginner. This is my first experience with Redux.
This is the error I've been getting when it should be displaying the home screen of my webpage.
Actions must be plain objects. Instead, the actual type was: 'string'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.
I have been searching everywhere but it looks to me like I applied thunk correctly. I'm hoping someone more experienced will be able to spot my mistake. Thank you.
HomeScreen.js
import React, { useEffect } from 'react';
import {Link} from 'react-router-dom';
import { listProducts } from '../actions/productActions.js';
import { useDispatch, useSelector } from 'react-redux';
function HomeScreen() {
const productList = useSelector(state => state.productList);
const { products, loading, error} = productList;
const dispatch = useDispatch();
useEffect(() => {
dispatch(listProducts());
return () => {
//
};
}, [])
return loading? <div>Loading...</div> :
error? <div>{error}</div>:
<ul className="products">
{
products.map(product =>
<li key={product._id}>
<div className="product">
<Link to={'/product/' + product._id}>
<img className="product-image" src={product.image} alt="product" />
</Link>
<div className="product-name">
<Link to={'/product/' + product._id}>{product.name}</Link>
</div>
<div className="product-brand">{product.brand}</div>
<div className="product-price">${product.price}</div>
<div className="product-rating">{product.rating} Stars ({product.numReviews} Reviews)</div>
</div>
</li>)
}
</ul>
}
export default HomeScreen;
store.js
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { productListReducer } from './reducers/productReducers.js';
import thunk from 'redux-thunk';
import * as compose from 'lodash.flowright';
const initialState = {};
const reducer = combineReducers({
productList: productListReducer,
})
const composeEnhancer = window.__REDUXDEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, initialState, composeEnhancer(applyMiddleware(thunk)));
export default store;
productActions.js
import { PRODUCT_LIST_FAIL, PRODUCT_LIST_REQUEST, PRODUCT_LIST_SUCCESS } from "../constants/productconstants.js";
import axios from "axios";
const listProducts = () => async (dispatch) => {
try {
dispatch(PRODUCT_LIST_REQUEST);
const {data} = await axios.get("/api/products");
dispatch({type: PRODUCT_LIST_SUCCESS, payload: data});
}
catch (error) {
dispatch({type: PRODUCT_LIST_FAIL, payload:error.message});
}
}
export {listProducts};
productReducers.js
import { PRODUCT_LIST_FAIL, PRODUCT_LIST_REQUEST, PRODUCT_LIST_SUCCESS } from "../constants/productconstants";
function productListReducer(state= {products: [] }, action) {
switch (action.type) {
case PRODUCT_LIST_REQUEST:
return {loading:true};
case PRODUCT_LIST_SUCCESS:
return {loading:false, products: action.payload};
case PRODUCT_LIST_FAIL:
return {loading:false, error: action.payload};
default:
return state;
}
}
export { productListReducer }
PRODUCT_LIST_REQUEST appears to be a string. You cannot dispatch a string by itself - only action objects. Actions are always objects that have a type field inside, like {type: 'counter/incremented'}.
That said, you should be using our official Redux Toolkit package to write your Redux code. Redux Toolkit will simplify all of the Redux store setup and reducer logic you've shown.

useContext returning undefined when using hook

So I am trying to create a kind of store using react context API and I ran into a problem that when I use the useContext it is returning undefined.
So the code I have is this:
StateProvider.js
import React, { createContext, useContext, useReducer } from "react";
//Needed to track the basket and the user info
//DATA LAYER
export const StateContext = createContext();
// BUILD PROVIDER
export const StateProvider = ({ reducer, initialState, children}) => (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
);
export const useStateValue = () => useContext(StateContext);
reducer.js
export const initialState = {
basket: ["asd", "asd"],
};
function reducer(state, action) {
switch(action.type){
case 'ADD_TO_BASKET':
//add item to basket
break;
case 'REMOVE_FROM_BASKET':
//remove item from basket
break;
default:
return state;
}
}
export default reducer;
index.js
import reducer, { initialState } from './state/reducer';
....
<StateProvider initalState={initialState} reducer={reducer}>
<App />
</StateProvider>
And the problem is on this file, where I try to console log the basket that I get from the reducer.js initalState.
Header.js
import { useStateValue } from '../state/StateProvider'
...
function Header() {
console.log(useContext(StateContext))
const [{ basket }]= useStateValue();
console.log(basket);
So the error is when I use the const [{ basket }]= useStateValue();, it says this : Cannot read property 'basket' of undefined.
The problem was on index.js, initialState was badly written and I was getting no error because of ES6.

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>

React: Context to pass state between two hierarchies of components

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.

react-redux TypeError: notes.map is not a function

Why am I getting TypeError: notes.map is not a function in the following part of my Notes component? {notes.map((note) => (
components/Notes.js
import React, { Component } from "react"
import { connect } from "react-redux"
const mapStateToProps = (state) => {
return { notes: state.notes }
}
const NotesList = ({ notes }) => (
<ul className="notes_list">
{notes.map((note) => (
<li className="note_body" key={note.id}>
<div dangerouslySetInnerHTML={{ __html: note.body }}></div>
</li>
))}
</ul>
)
const Notes = connect(mapStateToProps)(NotesList);
export default Notes;
reducers/notes.js
import * as types from '../actions/actionTypes'
const initialState = {
notes: [{id: 1, body: "hey"}]
}
function notes(state = initialState, action) {
switch (action.type) {
...
default:
return state
}
}
export default notes
root reducer
import { combineReducers } from 'redux'
import notes from './notes'
import noteForm from './noteForm'
const rootReducer = combineReducers({
notes,
noteForm
})
export default rootReducer
app.js
import React, { Component } from 'react';
import Notes from './components/Notes'
import NoteForm from './components/NoteForm'
const App = () => (
<div className="App">
<NoteForm />
<Notes />
</div>
)
export default App
---upd
store
import { createStore, applyMiddleware } from 'redux'
import rootReducer from '../reducers'
import {ping} from './enhancers/ping'
import thunk from 'redux-thunk'
export default function configureStore(initialState) {
const store = createStore(rootReducer, initialState, applyMiddleware(thunk, ping))
return store
}
index.js
...
import configureStore from './store/configureStore'
const store = configureStore()
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'));
Are you providing the connect function with a store? If so, everything looks fine to me -- it'd be useful to see your store initialization code.
Create a store with createStore from redux and wrap your App with a Provider from react-redux:
app.js
...
import notesReducer from './reducers/notes'
import { createStore } from 'redux'
const store = createStore(notesReducer) // use combineReducers when you add a 2nd reducer
const App = () => (
<Provider store={store}>
<div className="App">
<NoteForm />
<Notes />
</div>
</Provider>
)
If you already have a Provider somewhere else, check if everything's okay there.
Here's my fully working example - I copied your Notes.js file and wrote up the following App.js - no errors whatsoever (I bundled store creation and reducers all in one file for simplicity):
import React, { Component } from 'react';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import Notes from './Notes'
const initialState = {
notes: [{
id: 1,
body: 'testing'
}]
}
function notes(state = initialState, action) {
switch (action.type) {
default:
return state
}
}
const store = createStore(notes)
export default () => (
<Provider store={store}>
<Notes />
</Provider>
)
Update for combineReducers
When using combineReducers, your reducers' initialState will already be namespaced in the store under the key which was used in the combineReducers call. Change your notes reducer's initialState to an array:
import * as types from '../actions/actionTypes'
// no need for { notes: [] } here, combineReducers({ notes }) will take care of that
const initialState = [{ id: 1, body: 'hey' }]
function notes(state = initialState, action) {
switch (action.type) {
...
default:
return state
}
}
export default notes
When you get map isn't a function that means you're not calling the data correctly.
I see in notes reducer page you're not calling the states correctly
function notes(state = initialState, action) {
switch (action.type) {
...
default:
return state
}
}
Change it to:
function notes(state = initialState.notes, action) {
switch (action.type) {
...
default:
return state
}
}
The regular way to do this is to not putting your states in an array
const initialState = {
id: 1,
body: "hey"
}
function notes(state = initialState, action) {
switch (action.type) {
...
default:
return state
}
}
This will works fine
since my root reducer has the following structure
const rootReducer = combineReducers({
notes
})
I can reach notes by state.notes.notes
const mapStateToProps = (state) => {
return { notes: state.notes.notes }
}
having the following initial state structure for notes
const initialState = {
notes: []
}

Categories

Resources