problem in fetching data from a file by using useReducer in reactjs - javascript

I am new to reactjs and playing with useState and useReducer. I have data in a file and from that file I want to get data. I can do it with useState but when I want to do it by using useReducer I get an error. Another interesting thing is that, if I insert that same data in useReducer’s initial state it works and displays the data.
Below is the code and data file.
dataOnFile.js (file from which I want to fetch data)
const dataOnFile = [
{
id: 1,
name: "ahmad",
status: true,
},
{
id: 2,
name: "john",
status: true,
},
{
id: 3,
name: "sara",
status: true,
},
];
export default dataOnFile;
globalContext.js (using context to use it in other compnents, in this I am using useState and by using useState it works fine, it accepts the Items as an initial state)
import React, { createContext, useReducer, useState } from "react";
import Items from "./dataOnFile";
export const GlobalContext = createContext();
export function GlobalProvider(props) {
const [items, setItems] = useState(Items);
return (
<div>
<GlobalContext.Provider value={[items, setItems]}>
{props.children}
</GlobalContext.Provider>
</div>
);
}
GlobalContext.js (by using useReducer, I get an error, it's not accepting Items as an initial state)
ReferenceError: Cannot access 'Items' before initialization
import React, { createContext, useReducer, useState } from "react";
import Items from "./dataOnFile";
export const GlobalContext = createContext();
function itemsReducer(Items, action) {
return <div></div>;
}
export function GlobalProvider(props) {
const [Items, itemsDispatch] = useReducer(itemsReducer, Items);
return (
<div>
<GlobalContext.Provider value={[Items, itemsDispatch]}>
{props.children}
</GlobalContext.Provider>
</div>
);
}
globalContext.js (if I put data on useReducer’s initial state it works)
import React, { createContext, useReducer, useState } from "react";
import Items from "./dataOnFile";
export const GlobalContext = createContext();
function itemsReducer(Items, action) {
return <div></div>;
}
export function GlobalProvider(props) {
const [Items, itemsDispatch] = useReducer(itemsReducer, [
{
id: 1,
name: "ahmad",
status: true,
},
{
id: 2,
name: "sheeraz",
status: true,
},
]);
return (
<div>
<GlobalContext.Provider value={[Items, itemsDispatch]}>
{props.children}
</GlobalContext.Provider>
</div>
);
}

I think the problem here is that you're putting Items as the state for the reducer, the initial state and the parameter in the reducer. Note that useReducer calls the hook, thats where you want to pass your initial state.
Try this in the component:
const [items, itemsDispatch] = useReducer(itemsReducer, Items);
(Note that there is no capital 'I' in that state name)
And this in the reducer:
const itemsReducer = (state, action) => {
// Do stuff
}

Related

useContext is not displaying the updated state (when data is retrieved from firebase in useEffect hook in global file) in child component

I am using context api to have a global state of products, for this I have ProductsContext.js
I have an empty state of array defined like this in ProductsContext.js
const [products, setProducts] = useState([]);
In the same file (ProductsContext.js) I am using useEffect to retrive all the products from firebase and then updating the products state. Below is all the code of ProductsContext.js
import React, { createContext, useState, useEffect } from 'react'
import { db } from '../Config/Config'
export const ProductsContext = createContext();
export const ProductsContextProvider = (props) => {
// console.log(props);
const [products, setProducts] = useState([]);
useEffect(() => {
// console.log('use effect');
const prevProducts = products;
db.collection('Products').onSnapshot(snapshot => {
let changes = snapshot.docChanges();
changes.forEach(change => {
if (change.type === 'added') {
prevProducts.push({
ProductID: change.doc.id,
ProductName: change.doc.data().ProductName,
ProductDescription: change.doc.data().ProductDescription,
ProductPrice: change.doc.data().ProductPrice,
ProductImg: change.doc.data().ProductImg
})
}
// console.log(prevProducts);
setProducts(prevProducts);
console.log(products); // I am able to see the products in console
})
})
})
return (
<ProductsContext.Provider value={{ products: [...products] }}>
{props.children}
</ProductsContext.Provider>
)
}
My problem is Home.js is my child component and I am using useContext in this file but it is not returning the updated state but only returning an empty array
import React, { useContext } from 'react'
import { ProductsContext } from '../Global/ProductsContext'
export const Home = () => {
const { products } = useContext(ProductsContext);
console.log(products); // empty array
return (
<div>
</div>
)
}
this is my app.js
import React, { Component } from 'react'
import { ProductsContextProvider } from './Global/ProductsContext'
import { Home } from './Components/Home'
export class App extends Component {
render() {
return (
<ProductsContextProvider>
<Home />
</ProductsContextProvider>
)
}
}
export default App
Whenever your state is an object (or an array, but that's technically also an object), you must avoid mutating it.
In your code, you are mutating your products array, by calling push(...) on the original array instead of creating a copy of it.
Setting the state with a mutated array does not trigger an update because React compares the reference of the next array with the previous array and determines it to be equal.
To solve this, create a copy of the array and update the state with that:
const prevProducts = [...products];
I don't know how but when I changed my functional component (ProductsContext.js) to class based component with the same approach, it is somehow working and I am able to get the products in my child components. Here is the code
import React, { createContext } from 'react'
import { db } from '../Config/Config'
export const ProductsContext = createContext();
export class ProductsContextProvider extends React.Component {
state = {
products: []
}
componentDidMount() {
const prevProducts = this.state.products;
db.collection('Products').onSnapshot(snapshot => {
let changes = snapshot.docChanges();
changes.forEach(change => {
if (change.type === 'added') {
prevProducts.push({
ProductID: change.doc.id,
ProductName: change.doc.data().ProductName,
ProductDescription: change.doc.data().ProductDescription,
ProductPrice: change.doc.data().ProductPrice,
ProductImg: change.doc.data().ProductImg
})
}
// console.log(prevProducts);
this.setState({
products: prevProducts
})
// console.log(this.state.products);
})
})
}
render() {
return (
<ProductsContext.Provider value={{ products: [...this.state.products] }}>
{this.props.children}
</ProductsContext.Provider>
)
}
}

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 useContext and useMemo for Global variables

Would like to seek guidance from folks if this React implementation makes sense and understand the pitfalls if any. The implementation works but I am unsure if its the correct practice. Please kindly advise.
Idea - Create an AppContext that allows reusability of global states (or even functions) - instead of the conventional useContext + useReducer
AppContext.jsx
import React from 'react';
export const AppContext = React.createContext(null);
export const AppContextProvider = (props) => {
const [ appState, setAppState ] = React.useState({});
const appStateProvider = React.useMemo(() => ({ appState, setAppState }), [ appState, setAppState ]);
const setAppStateItem = (key, value) => {
appStateProvider.setAppState(state => { return { ...state, [key]: value} })
return value;
}
const getAppStateItem = (key = '', fallback) => {
return appState[key] || fallback;
}
const deleteAppStateItem = (key = '') => {
if(key in appState) {
appStateProvider.setAppState(state => {
state[key] = undefined;
return state;
})
}
}
return (
<AppContext.Provider value={{ appStateProvider, setAppStateItem, getAppStateItem, deleteAppStateItem }}>
{props.children}
</AppContext.Provider>
)
}
Create.jsx
import React from 'react';
import { AppContext } from 'contexts';
const { setAppStateItem } = React.useContext(AppContext);
....
setAppStateItem('count', 5);
....
Consume.jsx
import React from 'react';
import { AppContext } from 'contexts';
const { getAppStateItem, setAppStateItem } = React.useContext(AppContext);
....
const count = getAppStateItem('count');
....
Here was an approach to create a global state using useContext and useReducer following a pattern similar to redux. You essentially set up a Store with useReducer and a Context.Provider that you then wrap the rest of your application in. Here was a small implementation I had going:
import React, { createContext, useReducer } from "react";
import Reducer from './Reducer'
const initialState = {
openClose: false,
openOpen: false,
ticker: "BTCUSDT",
tickerRows: [],
positionRows: [],
prices: {},
walletRows: []
};
const Store = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
return (
<Context.Provider value={[state, dispatch]}>
{children}
</Context.Provider>
)
};
export const ACTIONS = {
SET_CLOSE_OPEN: 'SET_CLOSE_OPEN',
SET_OPEN_OPEN: 'SET_OPEN_OPEN',
SET_TICKER: 'SET_TICKER',
SET_TICKER_ROWS: 'SET_TICKER_ROWS',
SET_POSITION_ROWS: 'SET_POSITION_ROWS',
SET_PRICES: 'SET_PRICES',
SET_WALLET_ROWS: 'SET_WALLET_ROWS'
}
export const Context = createContext(initialState);
export default Store;
Here is the reducer:
import { ACTIONS } from './Store'
const Reducer = (state, action) => {
switch (action.type) {
case ACTIONS.SET_CLOSE_OPEN:
return {
...state,
openClose: action.payload
};
case ACTIONS.SET_OPEN_OPEN:
return {
...state,
openOpen: action.payload
};
...
default:
return state;
}
};
export default Reducer;
I put the Store component in index.js so that it's context is available to all of the components in the app.
ReactDOM.render(
<React.StrictMode>
<Store>
<App />
</Store>
</React.StrictMode>,
document.getElementById('root')
);
Then when you want to access and update the state, you just import the actions and context and the useContext hook:
import { useContext} from "react";
import { ACTIONS, Context } from './store/Store';
and then you just add
const [state, dispatch] = useContext(Context);
inside your functional component and you can access the state and use dispatch to update it.
One limitation is that every component that accesses the state with useContext re-renders every time anything in the state gets updated, not just the part of the state that the component depends on. One way around this is to use the useMemo hook to control when the component re-renders.
export default function WalletTable({ priceDecimals }) {
const classes = useStyles();
const [state, dispatch] = useContext(Context);
async function getCurrentRows() {
const response = await fetch("http://localhost:8000/wallet");
const data = await response.json();
dispatch({ type: ACTIONS.SET_WALLET_ROWS, payload: data });
}
useEffect(() => {
getCurrentRows();
}, []);
const MemoizedWalletTable = useMemo(() => {
return (
<TableContainer component={Paper}>
...
</TableContainer>
);
}, [state.walletRows])
return MemoizedWalletTable
}
Having to memoize everything makes it seem like maybe just using redux isn't all that much more complicated and is easier to deal with once set up.

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 state property undefined in function component using Context API

I am new to React's context API and hooks for function components. I am trying to pass state to a child component, ActivityTable.js. I wrapped the provider around the app (App.js), however state properties are still undefined in ActivityTable.js -- TypeError: Cannot read property 'id' of undefined.
Any guidance would be appreciated.
App.js
import ActivityState from "./context/activities/ActivityState";
const App = () => {
return (
<StylesProvider injectFirst>
<ContactState>
<ActivityState>
...
</ActivityState>
</ContactState>
</StylesProvider>
);
};
export default App;
ActivityState.js
import React, { useReducer } from 'react';
import ActivityContext from './ActivityContext';
import ActivityReducer from './ActivityReducer';
import { ADD_ACTIVITY, DELETE_ACTIVITY, SET_CURRENT_ACTIVITY } from '../types';
const ActivityState = props => {
const initialState = {
activities: [
{
id: 1,
activity_description: "a desc",
activity_name: "a",
},
{
id: 2,
activity_description: "b desc",
activity_name: "b",
},
{
id: 3,
activity_description: "c desc",
activity_name: "c",
}
]
};
const [state, dispatch] = useReducer(ActivityReducer, initialState);
const deleteActivity = id => {
dispatch({ type: DELETE_ACTIVITY, payload: id });
};
const setCurrentActivity = activity => {
dispatch({ type: SET_CURRENT_ACTIVITY, payload: activity });
};
return (
<ActivityContext.Provider
value={{
activities: state.activities,
deleteActivity,
setCurrentActivity
}}>
{ props.children }
</ActivityContext.Provider>
);
}
export default ActivityState;
ActivityContext.js
import { createContext } from "react";
const ActivityContext = createContext(null);
export default ActivityContext;
ActivityReducer.js
import { DELETE_ACTIVITY, SET_CURRENT_ACTIVITY } from '../types';
export default (state, action) => {
switch (action.type) {
case DELETE_ACTIVITY:
return {
...state,
activities: state.activities.filter(
activity => activity.id !== action.payload
)
};
case SET_CURRENT_ACTIVITY:
return {
...state,
current: action.payload
};
default:
return state;
}
};
ActivityView.js
import React, { useContext } from "react";
import ActivityContext from '../../context/activities/ActivityContext';
import ActivityTable from './ActivityTable';
const Activities = () => {
const activityContext = useContext(ActivityContext);
const { activities } = activityContext;
console.log('activities: ', activities);
return (
<div>
<ActivityTable/>
</div>
);
}
export default Activities;
ActivityTable.js
import React, { useContext, useState } from "react";
import ActivityContext from "../../context/activities/ActivityContext";
const ActivityTable = ({ activity }) => { //activity is undefined here
const activityContext = useContext(ActivityContext);
const { activities } = activityContext;
const { id, activity_name, activity_desc } = activity; //undefined
return (
<div>
<tr>
<td>{id}</td>
<td>{activity_name}</td>
<td>{activity_desc}</td>
</tr>
</div>
);
};
export default ActivityTable;
It looks like you're using activity as a prop inside ActivityTable, but never actually supplying that prop.
<ActivityTable activity={foo} />
I can't tell what data you're trying to pass to the table. You're importing the context successfully in both components, but never using the context data.

Categories

Resources