I have created a provider that is doing API call and setting data inside provider using value
//context
export const ProductContext = createContext({
loading: false,
data: null,
});
export function useProductContext() {
const context = useContext(ProductContext);
return context;
}
//provider
export const ProductProvider = ({ children, id }) => {
const { data, error, loading } = fetch({url},'id');
const value = useMemo(
() => ({
data
}),
[data],
);
return (
<ProductContext.Provider value={value}>
{children}
</ProductContext.Provider>
);
};
// component
const Card = (): JSX.Element => {
const { data } = useProductContext();
return (
<StyledSection>
<button onClick={}>fetch data with new params</button>
</StyledSection>
);
};
export default Card;
Here in component i want to fetch data when user click on button with different params.
So you want to fetch Data from the value you are given. Here is the way that I'll do this.
//Context
export const ProductContext = createContext();
export const ProductProvider = ({children}) => {
const fetchData = (val) => fetch(`fetch/${val}`);
return (
<ProductContext.Provider value={{fetchData}}>
{children}
</ProductContext.Provider>
);
};
As you can see in my context above I have my fetchData method there. Which accepts an argument.
Note: Make sure you have wrapped your root component with the above Provider.
In my component, I'll do it like this,
//Component
import {useContext} from 'react';
import {ProductContext} from '{your path here}/ProductContext';
const Card = () => {
const {fetchData} = useContext(ProductContext);
return (
<StyledSection>
<button onClick={() => fetchData('pass your value here')}>fetch data with new params</button>
</StyledSection>
);
}
export default Card
I believe this is the most simplest way you can achieve your task.
Related
I have a state that I want to make global so that I can use it across multiple different components
and I am trying to do this through using context.
So I have my initial Component which gets the data and sets the global state, the issue I am having is when I try to use this state in the other components it seems to be empty because I believe my GlobalContext varibale is not updating so will be empty when the other components try to use the state. I cannot seem to figure out what I am missing to ensure my global state and context are both updated so that I can use them across the different components that require the data as well.
Can anyone figure out where I should update my context as well as my state
Component that gets the data initially:
import React from "react";
import { useState, useEffect, useMemo, useContext } from "react";
import axios from "axios";
import { GlobalContext } from "./Store";
function Map() {
// ------- global state
const [activities, setActivities] = useContext(GlobalContext);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setActivitieData();
console.log("activities after useEffect", activities)
}, []);
const getActivityData = async () => {
console.log("calling")
const response = await axios.get(
"http://localhost:8800/api/"
);
return response.data;
};
const setActivitieData = async () => {
const activityData = await getActivityData();
setActivities(activityData);
console.log("Global activities state = ", activities);
};
return !isLoading ? (
<>
<MapComp
activityData={activities}
/>
</>
) : (
<div>
<p>Loading...</p>
</div>
);
}
export default Map;
GlobalStateStore component:
import React, {useState} from "react";
const initState = [];
export const GlobalContext = React.createContext();
const Store = ({children}) => {
const [activities, setActivities] = useState(initState);
return (
<GlobalContext.Provider value={[activities, setActivities]}>
{children}
</GlobalContext.Provider>
)
}
export default Store;
component I am trying to use the global state in but is empty:
import React, {useContext} from 'react';
import { GlobalContext } from "./Store";
function ActivityList() {
const [activities, setActivities] = useContext(GlobalContext);
let displayValues;
displayValues =
activities.map((activity) => {
return (
<div>
<p>{activity.name}</p>
<p>{activity.distance}m</p>
</div>
);
})
return (
<>
<p>Values</p>
{displayValues}
</>
);
}
export default ActivityList;
App.js:
function App() {
return (
<Store>
<div className="App">
<NavBar />
<AllRoutes />
</div>
</Store>
);
}
export default App;
Here's a barebones single-file version of your code that certainly works.
Since you aren't showing how you're mounting your <Map /> and <ActivityList /> components originally, there's not much more I can do to help you with that code, though I will note that it's useless to try and log activities in the same function that has just setActivities, since setState is async (and the function will have captured the earlier activities value anyway).
import React, { useContext, useState, useEffect } from "react";
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
async function getActivityData() {
console.log("calling");
await delay(1000);
return [{ name: "foo", distance: 123 }];
}
function Map() {
const [, setActivities] = useContext(GlobalContext);
useEffect(() => {
getActivityData().then(setActivities);
}, [setActivities]);
return <>map</>;
}
const initState = [];
const GlobalContext = React.createContext();
const Store = ({ children }) => {
const [activities, setActivities] = useState(initState);
return (
<GlobalContext.Provider value={[activities, setActivities]}>
{children}
</GlobalContext.Provider>
);
};
function ActivityList() {
const [activities] = useContext(GlobalContext);
return <div>{JSON.stringify(activities)}</div>;
}
export default function App() {
return (
<Store>
<Map />
<ActivityList />
</Store>
);
}
I am having a very tough time solving this issue
So I have set a context for cart from commerce.js and want to render it on a new page.
I am able to add to cart and all.
The cart object is getting picked in one page, but it to appear in a whole new page so therefore using contexts.
import {createContext, useEffect, useContext, useReducer} from 'react'
import {commerce} from '../../lib/commerce'
//need to correct this file to be a tsx file
const CartStateContext = createContext()
const CartDispatchContext = createContext()
const SET_CART = "SET_CART"
const initialState = {
total_items: 0,
total_unique_items: 0,
line_items: []
}
const reducer = (state,action) => {
switch(action.type){
case SET_CART:
return { ...state, ...action.payload }
default:
throw new Error(`Unknown action: ${action.type}` )
}
}
export const CartProvider = ({children}) => {
const [state, dispatch] = useReducer(reducer, initialState)
const setCart = (payload) => dispatch({type: SET_CART, payload})
useEffect(() => {
getCart()
},[])
const getCart = async() => {
try {
const cart = await commerce.cart.retrieve()
setCart(cart)
} catch (error){
console.log("error")
}
}
return (
<CartDispatchContext.Provider value = {{setCart}}>
<CartStateContext.Provider value = {state}>
{children}
</CartStateContext.Provider>
</CartDispatchContext.Provider>
)
}
export const useCartState = () => useContext (CartStateContext)
export const useCartDispatch = () => useContext (CartDispatchContext)
my _app.tsx
const MyApp = ({ Component, pageProps }: AppProps) => (
<div>
<Web3ContextProvider>
<>
<Component {...pageProps} />
<ToastContainer
hideProgressBar
position="bottom-right"
autoClose={2000}
/>
</>
</Web3ContextProvider>
<CartProvider>
<>
<Component {...pageProps} />
</>
</CartProvider>
</div>
);
export default MyApp;
When I try to render it on the cartPage
import React from "react"
import {CartItem, CartContainer, NavigationBar } from "../components"
import {useRouter} from 'next/router'
import { useCartState, useCartDispatch } from "../context/Cart"
export const CartPage = () => {
const {setCart} = useCartDispatch()
// console.log(setCart)
const {line_items} = useCartState()
// console.log(line_items)
return(
<pre>
{JSON.stringify(line_items,null,2)}
</pre>
)
}
export default CartPage
I get the above Type error
I am not able to figure out what is going wrong. Also if you folks have any other suggestion in next how do i render the cart object in another page
I'm building a custom dropdown component and i'm using redux toolkit to manage the state. It works just fine
But when I reuse the dropdown component in another place, in the same page, the "states conflicts", so when I open one dropdown, the another opens to. (This is my dropdown reducer)
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
interface Dropdown {
isOpen: boolean;
selectedValue: string;
}
export const toggleOpen = createAction("dropdown/toggleOpen");
export const changeSelectedValue = createAction<string>(
"dropdown/changeSelectedValue"
);
const initialState = {
isOpen: false,
selectedValue: "-- Selecione --",
} as Dropdown;
const dropdownReducer = createReducer(initialState, (builder) => {
builder
.addCase(toggleOpen, (state) => {
state.isOpen = !state.isOpen;
})
.addCase(changeSelectedValue, (state, action) => {
state.selectedValue = action.payload;
});
});
const dropdownStore = configureStore({
reducer: dropdownReducer,
});
type RootState = ReturnType<typeof dropdownStore.getState>;
type AppDispatch = typeof dropdownStore.dispatch;
export const useDropdownDispatch = () => useDispatch<AppDispatch>();
export const useDropdownSelector: TypedUseSelectorHook<RootState> = useSelector;
export default dropdownStore;
Is there any way that I can create different "instances" of the same store, so each dropdown has it's own?
PS: I'm populating the Provider in the Dropdown component, so there is one provider to each dropdown, as follow:
import React from "react";
import { Provider } from "react-redux";
import ArrowDown from "../assets/icons/arrow-down";
import ArrowUp from "../assets/icons/arrow-up";
import store, {
useDropdownSelector,
useDropdownDispatch,
toggleOpen,
changeSelectedValue,
} from "../store/reducers/dropdown";
import styles from "./SingleDropdown.module.scss";
interface ItemProps {
value: string;
onClick?: (value: string) => void;
}
const ArrowIcon = () => {
const isOpen = useDropdownSelector((state) => state.isOpen);
return isOpen ? <ArrowUp /> : <ArrowDown />;
};
export const SelectItem: React.FC<ItemProps> = ({
children,
value,
onClick,
}) => {
const dispatch = useDropdownDispatch();
const changeSelectedValueClickHandler = () => {
dispatch(changeSelectedValue(value));
if (onClick) onClick(value);
};
return (
<div
className={styles.dropdown__menu__items}
onClick={changeSelectedValueClickHandler}
id={value}
>
{children}
</div>
);
};
const SelectMenu: React.FC = ({ children }) => {
const isOpen = useDropdownSelector((state) => state.isOpen);
return isOpen ? (
<div className={styles.dropdown__menu}>{children}</div>
) : null;
};
const InitialSelectItem = () => {
const selectedValue = useDropdownSelector((state) => state.selectedValue);
const dispatch = useDropdownDispatch();
return (
<div
onClick={() => dispatch(toggleOpen())}
className={styles.dropdown__field}
>
{selectedValue}
<ArrowIcon />
</div>
);
};
export const SingleSelect: React.FC = ({ children }) => {
return (
<Provider store={store}>
<div className={styles.dropdown}>
<InitialSelectItem />
<SelectMenu>{children}</SelectMenu>
</div>
</Provider>
);
};
Generally, we would suggest not keeping state like this in Redux, for exactly the kind of reason you just saw. It isn't "global" state - only one specific component cares about it:
https://redux.js.org/tutorials/essentials/part-2-app-structure#component-state-and-forms
By now you might be wondering, "Do I always have to put all my app's state into the Redux store?"
The answer is NO. Global state that is needed across the app should go in the Redux store. State that's only needed in one place should be kept in component state.
If you truly need to have this data in Redux, and control multiple "instances" of a component with their own separate bits of state, you could use some kind of a normalized state structure and track the data for each component based on its ID.
I am using jest and react-testing-library to write tests for my react application. In the application, I have a context provider that contains the state across most of the application. In my tests, I need to mock one state variable in context provider and leave one alone, but I'm not sure how to do this.
AppContext.tsx
const empty = (): void => {};
export const emptyApi: ApiList = {
setStep: empty,
callUsers: empty,
}
export const defaultState: StateList = {
userList = [],
step = 0,
}
const AppContext = createContext<ContextProps>({ api: emptyApi, state: defaultState });
export const AppContextProvider: React.FC<Props> = props => {
const [stepNum, setStepNum] = React.useState(0);
const [users, setUsers] = useState<User[]>([]);
const api: ApiList = {
setStep: setStepNum,
callUsers: callUsers,
}
const state: Statelist = {
userList: users,
step: stepNum,
}
const callUsers = () => {
const usersResponse = ... // call users api - invoked somewhere else in application
setUsers(userResponse);
}
return <AppContext.Provider value={{ api, state}}>{children}</AppContext.Provider>;
}
export default AppContext
In _app.tsx
import { AppContextProvider } from '../src/context/AppContext';
import { AppProps } from 'next/app';
import { NextPage } from 'next';
const app: NextPage<AppProps> = props => {
const { Component, pageProps } = props;
return (
<React.Fragment>
<AppContextProvider>
<Component {...pageProps}
</AppContextProvider>
</React.Fragment>
)
}
export default app
the component that uses AppContext
progress.tsx
import AppContext from './context/AppContext';
const progress: React.FC<Props> = props => {
const { state: appState, api: appApi } = useContext(AppContext);
const { userList, step } = appState;
const { setStep } = appApi;
return (
<div>
<Name />
{step > 0 && <Date /> }
{step > 1 && <UserStep list={userList} /> }
{step > 2 && <Address />
</div>
)
}
export default progress
users is data that comes in from an API which I would like to mock. This data is shown in progress.tsx.
stepNum controls the display of subcomponents in progress.tsx. It is incremented after a step is completed, once incremented, the next step will show.
In my test, I have tried the following for rendering -
progress.test.tsx
import progress from './progress'
import AppContext, { emptyApi, defaultState } from './context/AppContext'
import { render } from "#testing-library/react"
describe('progress', () => ({
const api = emptyApi;
const state = defaultState;
it('should go through the steps', () => ({
state.usersList = {...}
render(
<AppContext.Provider value={{api, state}}>
<progress />
</AppContext.Provider>
)
// interact with screen...
// expect(...)
})
})
However, when I set up the context provider like that in the test, I can set the userList to whatever I want, but it'll also override the setStep state hook so in the component, it won't update the state.
Is it possible to mock only the users variable inside of AppContext with jest, but leave users hook alone?
This is my component:
export default const NotesComponent = () => {
return notesList.map((noteProps) => (
<NewsCard {...notesProps} key={notesProps.id} />
))
}
Here is the place where i use this component:
const Notes = (props: NotesProps) => {
return (
<Container>
<NotesComponent />
</Container>
)
}
const mapStateToProps = (state) => ({
notesData: state.notes.notesData,
})
// and other code so mapStateToProps is working correctly
I don't know how to pass notesData to NotesComponent, so the NewsCard can read the data.
You can use connect high-order-function from react-redux and export the returned component:
import { connect } from 'react-redux'
// Redux state data notesData will be available in props
const NotesComponent = ({notesData}) => {
return notesData.map((noteProps) => (
<NewsCard {...noteProps} key={noteProps.id} />
))
}
const mapStateToProps = (state) => ({
notesData: state.notes.notesData,
})
export default connect(mapStateToProps)(NotesComponent)
Or, as NotesComponent is a function component, you can use react-hook useSelector instead of using connect to read redux data:
// in a function component
import { useSelector } from 'react-redux'
...
const notesData = useSelector((state) => state.notes.notesData)
Edit:
You can also connect parent component i.e. Notes with Redux and pass data to NotesComponent in props (to make NotesComponent a dumb or reusable component):
interface NotesProps {
notesData: write_your_type[]
// ...
}
const Notes = (props: NotesProps) => {
const { notesData } = props
return (
<Container>
<NotesComponent data={notesData} />
</Container>
)
}
const mapStateToProps = (state) => ({
notesData: state.notes.notesData,
})
export default connect(mapStateToProps)(Notes)
// it now exports enhanced (with redux data in props) Notes component
And, NotesComponent:
export default const NotesComponent = ({data}) => {
return data.map((item) => (
<NewsCard {...item} key={item.id} />
))
}