Add value to React context from child components - javascript

I am trying to use React context as a state manager in my React Native app.
Here's the context:
import React, { createContext, useState, useEffect } from "react";
import axios from "axios";
export const GlobalContext = createContext();
export const Provider = ({ children }) => {
const [tracksList, setTracksList] = useState([
{
track_list: []
}
]);
useEffect(() => {
axios
.get(
`https://cors-anywhere.herokuapp.com/https://api.musixmatch.com/ws/1.1/chart.tracks.get?page=1&page_size=10&country=us&f_has_lyrics=1&apikey=${
process.env.REACT_APP_MM_KEY
}`
)
.then(res => {
setTracksList([
{
track_list: res.data.message.body.track_list
}
]);
})
.catch(err => console.log(err));
}, []);
return (
<GlobalContext.Provider value={[tracksList, setTracksList]}>
{children}
</GlobalContext.Provider>
);
};
export const Consumer = GlobalContext.Consumer;
Child component. Here I'd like to make an API call to get users and set this users field to global context. I can get context value from consumer, but how to set the new one?
import React, { useContext } from "react";
import { GlobalContext } from "../../context/context";
const Demo = () => {
const contextValue = useContext(GlobalContext);
console.log(contextValue, "Context outside from JSX");
return <div>Content</div>;
};
export default Demo;
So, is it possible to add new value to React context from every child component, like in Redux? Thanks in advance!

You could use the useReducer effect to achieve Redux reducers:
// Create context
export const ApiContext = React.createContext();
// Create some reducer function
const reducer = (state, action) => {
if (action.type === 'some action name') {
return {
...state,
report: action.payload,
};
}
return state;
};
// Overwrite a context provider
const Provider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, {});
return (
<ApiContext.Provider
value={{
...state,
dispatch,
}}
>
{children}
</ApiContext.Provider>
);
};
Then you could use in components as follows:
const Component = () => {
const { dispatch, report } = React.useContext(ApiContext);
React.useEffect(() => {
const asyncPost = async () => {
const response = await fetch('some endpoint', {
method: 'POST',
});
const payload = await response.json();
// This will call a reducer action and update a state
dispatch({
type: 'some action name',
payload,
});
}
}, []);
...
};
So when Component is mounted, the state would be an empty object. Then when you update the state using the some action name action, the state becomes { report: some data from fetch }.

Related

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.

Using redux with react-hooks without the connect HOC

Currently, I'm using functional components with hooks but still dispatching my actions with the connect HOC.
I read through the documentation with useDispatch but I'm unsure how to incorporate it in my code. From the examples, they are passing the the action types and payloads inside the component. Would I have to move myOfferActions functions back to the component in order to useDispatch?
MyOffers component
import React, { useEffect } from "react";
import { connect, useSelector } from "react-redux";
import "./MyOffers.scss";
import MyOfferCard from "../../components/MyOfferCard/MyOfferCard";
import { fetchMyOffers } from "../../store/actions/myOffersActions";
const MyOffers = (props) => {
const myOffers = useSelector((state) => state.myOffers.myOffers);
useEffect(() => {
props.fetchMyOffers();
}, []);
return (
<div className="my-offers-main">
<h1>My offers</h1>
{myOffers && (
<div className="my-offer-container">
{myOffers.map((offer) => (
<MyOfferCard key={offer.id} offer={offer} />
))}
</div>
)}
</div>
);
};
export default connect(null, { fetchMyOffers })(MyOffers);
offerActions
export const fetchMyOffers = () => async (dispatch) => {
const userId = localStorage.getItem("userId");
try {
const result = await axiosWithAuth().get(`/offers/${userId}`);
let updatedData = result.data.map((offer) => {
//doing some stuff
};
});
dispatch(updateAction(FETCH_MY_OFFERS, updatedData));
} catch (error) {
console.log(error);
}
};
offerReducer
import * as types from "../actions/myOffersActions";
const initialState = {
offerForm: {},
myOffers: [],
};
function myOffersReducer(state = initialState, action) {
switch (action.type) {
case types.FETCH_MY_OFFERS:
return {
...state,
myOffers: action.payload,
};
default:
return state;
}
}
export default myOffersReducer;
I don't think you need connect when using the redux hooks.
You just need to call useDispatch like:
const dispatch = useDispatch();
And use the function by providing the object identifying the action:
dispatch({ type: 'SOME_ACTION', payload: 'my payload'});
It should be working with redux-thunk too (I guess this is what you're using): dispatch(fetchMyOffers())

How to use useEffect() to fetch data from an api in a useReducer() and useContext() state management setup?

reducer.js
export const reducer = (state, action) => {
switch(action.type) {
case SET_HEROES:
return { ...state, heroes: action.heroes }
}
}
AppContext.js
export const AppContext = React.createContext()
export const AppProvider = (props) => {
const initialState = {
heroes: []
}
const [appState, dispatch] = useReducer(reducer, initialState)
const setHeroes = async () => {
const result = await getHeroes()
dispatch({ type: SET_HEROES, heroes: result })
}
return <AppContext.Provider
values={{ heroes: appState.heroes, setHeroes }}
>
{props.children}
</AppContext.Provider>
}
HeroesScreen.js
const HeroesScreen = () => {
const { heroes, setHeroes } = useContext(AppContext)
useEffect(() => {
setHeroes()
}, [])
return <>
// iterate hero list
</>
}
export default HeroesScreen
Above is plain simple setup of a component using reducer + context as state management. Heroes are showing on the screen, everything works fine but I'm having a warning Reach Hook useEffect has a missing dependency: 'setHeroes'. But if I add it in as a dependency, it'll crash my app with Maximum depth update exceeded
Been searching but all I see is putting the function fetch call inside the useEffect(). What I would like is to extract the function and put it in a separate file following the SRP principle
EDITED:
As advised on using useCallback()
AppContext.js
const setHeroes = useCallback(() => {
getHeroes().then(result => dispatch({ type: SET_HEROES, heroes: result }))
}, [dispatch, getHeroes])
HeroesScreen.js
useEffect(() => {
setHeroes()
}, [setHeroes])
Adding the getHeroes as dependency on useCallback, linter shows unnecessary dependency
If you look at the code in AppProvider, you are creating a new setHeroes function every time it renders. So if you add setHeroes as a dependency to useEffect the code executes something like this:
AppProvider renders, setHeroes is created, the state is initial state
Somewhere down the component hierarchy HeroesScreen renders. useEffect is called which in turn calls setHeroes
getHeroes is called and an action is dispatched
reducer changes the state which causes AppProvider to re-render
AppProvider renders, setHeroes is created from scratch
The useEffect executes again since setHeroes changed and the whole loop repeats forever!
To fix the issue you indeed need to add setHeroes as a dependency to useEffect but then wrap it using useCallback:
const setHeroes = useCallback(async () => {
const result = await getHeroes();
dispatch({ type: SET_HEROES, heroes: result });
}, [getHeroes, dispatch]);
Good question, I also had this problem, this is my solution. I'm using Typescript but it'll work with JS only as well.
UserProvider.tsx
import * as React from 'react'
import { useReducer, useEffect } from 'react'
import UserContext from './UserContext'
import { getUser } from './actions/profile'
import userReducer, { SET_USER } from './UserReducer'
export default ({ children }: { children?: React.ReactNode }) => {
const [state, dispatch] = useReducer(userReducer, {})
const getUserData = async () => {
const { user } = await getUser()
dispatch({ type: SET_USER, payload: { ...user } })
}
useEffect(() => {
getUserData()
}, [])
return (
<UserContext.Provider value={{ user: state }}>
{children}
</UserContext.Provider>
)
}
Then wrap your App with the provider
index.tsx
<UserProvider>
<App />
</UserProvider>
Then to use the Context Consumer I do this
AnyComponent.tsx
<UserConsumer>
{({ user }) => {
...
}}
</UserConsumer
or you can also use it like this
const { user } = useContext(UserContext)
Let me know if it works.

ReactJS: How to correctly pass variables with fetch results through context?

Need help, just started to learn React. I'm trying to pass variables with json data to a component for further use, but catching the errors. what should I change to use variables with json() data from Store.js in the product.js component? THanks for your time!
https://jsfiddle.net/constant101/xu7zdn26/3/ for better visibility
//Store export(receiving data from the server and assigning them to variables)
import React, {useState, useEffect} from 'react'
import axios from 'axios'
export const ListContext = React.createContext([]);
export const ItemContext = React.createContext([]);
function Store() {
const [storeProducts, setStoreProducts] = useState([]);
const [detailProduct, setDetailProduct] = useState([]);
useEffect(() => {
axios.get('/products/')
.then(res => {
console.log(res)
setStoreProducts(res.data)
})
},[])
console.log('storeProducts:', storeProducts)
useEffect(() => {
axios.get('/products/:productId')
.then(res => {
console.log(res)
setDetailProduct(res.data)
})
},[])
console.log('detail product:', detailProduct)
return (
<ListContext.Provider value={[storeProducts, setStoreProducts]}>
<ItemContext.Provider value={[detailProduct, setDetailProduct]}>
<product/>
</ItemContext.Provider>
</ListContext.Provider>
);
}
export const detailProduct
//product.js ( file that uses data from the fetch)
import React, { useReducer, createContext, useContext, useState } from 'react';
import {ListContext, ItemContext } from '../Store';
import { useProductActions } from '../actions';
import { SET_PRODUCT_DETAILS } from '../actions/types';
const [storeProducts] = useContext(ListContext);
const [detailProduct] = useContext(ItemContext);
let tempProducts = [];
storeProducts.forEach(item => tempProducts.push({ ...item })
);
const initialState = {
products: tempProducts,
productDetails: { ...detailProduct }
};
console.log(storeProducts)
const productReducer = (state, action) => {
switch (action.type) {
case SET_PRODUCT_DETAILS:
return {
...state,
productDetails: action.payload
};
default:
throw new Error('Invalid action type');
}
};
export const ProductContext = createContext(initialState);
export const useProductState = () => {
return useContext(ProductContext);
};
export const ProductProvider = ({ children }) => {
const [state, dispatch] = useReducer(productReducer, initialState);
const productActions = useProductActions(state, dispatch);
return (
<ProductContext.Provider value={{ productState: state, productActions }}>
{children}
</ProductContext.Provider>
);
};
Well, assuming your request is right, i saw a syntax mistake. You should pass
<ListContext.Provider value={{storeProducts, setStoreProducts}}> instead of
<ListContext.Provider value={[storeProducts, setStoreProducts]}>
The reason:
a provider requires a prop called value with an Object inside.
In that case, you were passing an array.
it would be the same if you did:
<ListContext.Provider
value={{
storeProducts: storeProducts,
setStoreProducts: setStoreProducts
}}
>
but to follow the DRY principle, it's recommended to do that way described earlier

Redux Dev tools updating. Console not updating with state changes or data

Ive been trying to do this with react hooks and the useSelector/useDispatch. What happens is, I am able to see the data and state change in the Redux DevTools however, when logging to the console, I either get an empty array or undefined. I am also not able to render the data to the screen expectedly.
Posts Component
import React, {useState, useEffect} from 'react'
import PropTypes from 'prop-types'
import {useSelector, useDispatch} from 'react-redux'
import {getPosts} from '../actions/postActions'
const Posts = props =>{
const dispatch = useDispatch();
const postData = useSelector(state=> state.items, []) || []; //memoization?
const [items, setItems] = useState(postData)
console.log(postData);
useEffect(() => {
dispatch(getPosts());
}, []);
return(
<h1>{postData[0]}</h1>
)
}
export default Posts
ACTIONS
import {GET_POSTS, NEW_POSTS} from '../actions/types'
export const getPosts =()=> dispatch =>{
//fetch
console.log('fetching')
const url = 'https://jsonplaceholder.typicode.com/posts/'
fetch(url)
.then(res => res.json())
.then(posts=> dispatch({type: GET_POSTS, payload: posts}))
}
reduxDevTools image
I think the problem is coming from this line:
const postData = useSelector(state=> state.items, []) || [];
If you want postData to initially be an array, it's best to set it as an array in your reducer.
Working example (click Posts tab to make API call):
actions/postActions.js
import api from "../utils/api";
import * as types from "../types";
export const getPosts = () => async dispatch => {
try {
dispatch({ type: types.POSTS_FETCH });
const res = await api.get("posts");
dispatch({
type: types.POSTS_SET,
payload: res.data
});
} catch (err) {
dispatch({
type: types.POSTS_FAILED_FETCH,
payload: err.toString()
});
}
};
reducers/postsReducer.js
import * as types from "../types";
const initialState = {
data: [],
error: "",
isLoading: true
};
export default (state = initialState, { type, payload }) => {
switch (type) {
case types.POSTS_FETCH:
return initialState;
case types.POSTS_SET:
return {
...state,
data: payload,
error: "",
isLoading: false
};
case types.POSTS_FAILED_FETCH:
return {
...state,
error: payload,
isLoading: false
};
default:
return state;
}
};
containers/FetchPostsHooks/index.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getPosts } from "../../actions/postActions";
import Spinner from "../../components/Spinner";
import DisplayPosts from "../../components/DisplayPosts";
const Posts = () => {
const dispatch = useDispatch();
const { isLoading, data, error } = useSelector(state => state.posts, []);
useEffect(() => {
dispatch(getPosts());
}, [dispatch]);
return isLoading ? (
<Spinner />
) : error ? (
<p>{error}</p>
) : (
<DisplayPosts data={data} />
);
};
export default Posts;

Categories

Resources