trigger Axios data fetch on button click in another component - reactjs

I have component structure like this
src
--App
--DataTableComponent
--ButtonComponnet
axios
--useAxios
On app load I am calling Axios library in DataTableComponent (through custom hook useAxios) to fetch some data, then later when user clicks a button in ButtonComponent I would like Axios to load data again in DataTableComponent, but I am not sure how as the components are siblings.

In your situation what you'll want to do is lift state up. Here a link to the official react documentation
In addition, what I've done is created a code sandbox sample for your particular situation that demonstrates how you could do some thing similar in your app. Here's the link to that code sandbox https://codesandbox.io/s/romantic-brook-sfvpd?file=/src/App.tsx
The principle behind lifting state up is that if you have 2 or more components that must share information then lift that information one level up to their parent. So, what I do, as you see in the code sandbox is that the app component now gets the information and pushes it down to your data table component.
So, you'll want your useAxios hook to live in your parent app component. The button components job is to simply trigger the fetch.
Once the fetch is triggered, new data is returned by the useAxios hook.
The fetch also causes the App's useEffect hook to run and this updates the state and pushes the data to the data table component.
So, in your case you'll probably want to wrap your useAxios hook in your own custom hook so you can pass parameters which in turn your useAxios hook can use to fetch data from your API.
Continue to click on the fetch data button and each time I return a random number of items so you'll see your data components data getting updated. Remember to open the console to see the useAxios hook getting called followed by the data table contents being updated.
I have used a similar approach in some of my production apps and in those apps I've created similar custom wrapper around useSWR hooks
Using redux or some thing similar is a good idea if you have data that must be shared across the application i.e. global data but data that is specific to a few components doesn't need that approach and one should then go with the "lift state up" way of doing things.
For completeness the code is also given below.
import { useEffect, useState } from "react";
import "./styles.css";
// useAxios hook
const useAxios = (_: boolean) => {
console.log("useAxios");
// mock data
const arr = [
{
name: "Bianca Paul",
phone: "1-453-676-9140",
email: "mollis.integer#google.ca",
address: "221-3571 Nam Street",
id: 8
},
{
name: "Hadley Gordon",
phone: "1-235-486-3229",
email: "adipiscing.mauris#icloud.com",
address: "3255 Nec, Road",
id: 3
},
{
name: "Irma Bryan",
phone: "1-818-417-5465",
email: "ornare.in#icloud.net",
address: "136-222 Facilisis Rd.",
id: 2
},
{
name: "Simon Nash",
phone: "1-872-216-6482",
email: "enim.nec#aol.couk",
address: "Ap #873-5860 Erat St.",
id: 101
},
{
name: "Ursula Fleming",
phone: "(998) 407-7291",
email: "semper.erat#protonmail.com",
address: "110-1550 Phasellus Ave",
id: 43
}
];
// Randomize the data
function getRandomItem() {
// get random index value
let randomIndex = Math.floor(Math.random() * arr.length);
if (randomIndex === 0) randomIndex = 1;
// get random items
const item = arr.slice(0, randomIndex);
return item;
}
// return a promise
const data = new Promise<any>((resolve, reject) => {
setTimeout(() => {
return resolve(getRandomItem());
}, 1000);
});
return { data };
};
// Button component
const ButtonComponent = (props: { clickCallback: () => void }) => {
return (
<>
<button onClick={props.clickCallback}>fetch data</button>
</>
);
};
// DataComponent
const DataTableComponent = ({
data
}: {
data:
| [
{
name: string;
phone: string;
email: string;
address: string;
id: string;
}
]
| null;
}) => {
return (
<>
{data ? (
data.map((v) => (
<div key={v.id}>
{v.name},{v.address}, {v.phone}
</div>
))
) : (
<span>loading</span>
)}
</>
);
};
// App Parent component
export default function App(): JSX.Element {
const [fetch, setFetch] = useState(true);
const [returnedData, setReturnedData] = useState<
| [
{
name: string;
phone: string;
email: string;
address: string;
id: string;
}
]
| null
>(null);
const { data } = useAxios(fetch);
const buttonClicked = () => {
setFetch(true);
};
useEffect(() => {
let isMounted = true;
if (fetch) {
(async () => {
if (isMounted) setReturnedData(await data);
if (isMounted) setFetch(false);
console.log(await data);
})();
}
return function () {
isMounted = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetch]);
return (
<div className="App">
<h1>Lift state up</h1>
<div>
<DataTableComponent data={returnedData} />
</div>
<div>
<ButtonComponent clickCallback={buttonClicked} />
</div>
</div>
);
}

You can just send some prop var or function that will return true or something when button is clicked. Do you call ButtonComponnent in DataTableComponent or somewhere else ?

What you're trying to do is get two sibling components to communicate and call functions from each other, if I'm understanding correctly. If they're siblings, I'd recommend using a third-party library like Redux or EventHub
If you have Component A with the function "updateData", then you can call it in Component B using Redux by setting up a reducer called "updateData", and having DataTableComponent subscribe to the "data"
Redux file:
import { createStore, combineReducers } from "redux";
const reducer = (state = {}, action) => {
... {axios fetch here}
};
const store = createStore(reducer);
export default store;
DataTableComponent.jsx:
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import axios from "axios";
import { updateData } from "../redux/actions";
const DataTableComponent = () => {
const data = useSelector(state => state.data);
const dispatch = useDispatch();
const fetchData = () => dispatch(updateData());
return (
<div>
<button onClick={fetchData}>Fetch Data</button>
<table>{data.map((item, index) => <tr key={index}><td>{item.id}</td><td>{item.name}</td></tr>)}</table>
</div>
);
};
ButtonComponent.jsx:
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import axios from "axios";
import { updateData } from "../redux/actions";
const ButtonComponent = () => {
const data = useSelector(state => state.data);
const dispatch = useDispatch();
const fetchData = () => dispatch(updateData());
return (
<div>
<button onClick={fetchData}>Fetch Data</button>
<table>{data.map((item, index) => <tr key={index}><td>{item.id}</td><td>{item.name}</td></tr>)}</table>
</div>
);
};

Related

Maintaining multiple user inputs to antd Select in a single useState variable

I want to store the information of multiple inputs entered into antd Select components in a single state variable but am having trouble getting the below to work.
This example is solved here for a form but the same solution doesn't seem to work for antd Select component. There are two inputs: a first name and a last name that I want to remember. The below code doesn't work because e doesn't have an attribute called name is what the console tells me. I also tried e.target.name and e.target.value but I get an error that e doesn't have an attribute called a target either. What is the right way to do this?
import React, { useState } from 'react';
import { Select } from 'antd';
const App = () =>{
const [varState, setVarState] = useState({firstName:'Jack', lastName:'Smith'});
const firstNameOptions = [ {label:'Jack', value:'Jack'}, {label:'Jill',value:'Jill'}, {label:'Bill',value:'Bill'} ];
const lastNameOptions = [ {label:'Smith', value:'Smith'}, {label:'Potter',value:'Potter'}, {label:'Bach',value:'Bach'} ];
const changeState = (e) => {
setVarState( prevState => ({ ...prevState, [e.name]: e.value}));
console.log(varState)
};
return ( <>
<div>
<Select name={'firstName'} defaultValue={'Pick One'} options={firstNameOptions} onChange={changeState} />
<Select name={'lastName'} defaultValue={'Pick One'} options={lastNameOptions} onChange={changeState} />
</div>
</>
);
}
export default App;
At the heart of it, it seems that I don't know how to name the Select components in such a way that their names can be passed on to the onChange handler.
More generally, given a component like antd Select, how can I figure out what the right "name field" is for this component so that it's value can be passed on to an onChange handler? For instance, what in the documentation for select gives this information?
The Select component seems to be sending the value instead of the events object. So, You can just make a closure and pass the name of the select. Also, for consoling you can make use of a useEffect which only consoles when the state has been updated. Otherwise, you could see previous state as state updates are asynchronous. Below is a working solution.
import React, { useEffect, useState } from "react";
import { Select } from "antd";
const App = () => {
const [varState, setVarState] = useState({
firstName: "Jack",
lastName: "Smith"
});
const firstNameOptions = [
{ label: "Jack", value: "Jack" },
{ label: "Jill", value: "Jill" },
{ label: "Bill", value: "Bill" }
];
const lastNameOptions = [
{ label: "Smith", value: "Smith" },
{ label: "Potter", value: "Potter" },
{ label: "Bach", value: "Bach" }
];
// for consoling when the state updates
useEffect(() => {
console.log(varState);
}, [varState.firstName, varState.lastName]);
const changeState = (value, identifier) => {
// console.log(value, identifier);
setVarState((prevState) => ({ ...prevState, [identifier]: value }));
};
return (
<>
<div>
<Select
name={"firstName"}
defaultValue={"Pick One"}
options={firstNameOptions}
onChange={(val) => changeState(val, "firstName")}
/>
<Select
name={"lastName"}
defaultValue={"Pick One"}
options={lastNameOptions}
onChange={(val) => changeState(val, "lastName")}
/>
</div>
</>
);
};
export default App;
yes, Actually antd doesn't have attribute name for input fields. antdesign directly gives the selected value, we need to do some tweeks to achieve this.
Here is the solution:
import React, { useState } from 'react';
import { Select } from 'antd';
const firstNameOptions = [ {label:'Jack', value:'Jack'}, {label:'Jill',value:'Jill'}, {label:'Bill',value:'Bill'} ];
const lastNameOptions = [ {label:'Smith', value:'Smith'}, {label:'Potter',value:'Potter'}, {label:'Bach',value:'Bach'} ];
const App = () =>{
const [varState, setVarState] = useState(null);
const changeState = (fieldName) => (value) => {
setVarState( prevState => ({ ...prevState, [fieldName]: value}));
console.log(varState)
};
return ( <>
<div>
<Select defaultValue={'Pick One'} options={firstNameOptions} onChange={changeState('firstName')} />
<Select defaultValue={'Pick One'} options={lastNameOptions} onChange={changeState('lastName')} />
</div>
</>
);
}
export default App;
I hope this helps 😊

How do I use multiple Query hooks from redux-toolkit in the same component?

So in redux toolkit you're able to create hooks that call to a RestApi using multiple endpoints, I'm using 3 endpoints which using redux-toolkit it creates 3 hooks that I can use anywhere in my react application, my question is how do I get it to work all in one component?
import React from "react";
import { useSelector } from "react-redux";
import { useGetCountryQuery, useGetRegionQuery, useGetAllQuery } from "../../services/CountryAPI";
import CountryCard from "./CountryCard";
import { Link } from "react-router-dom";
const CountryList = () => {
const { option } = useSelector((state) => state.option);
const { data = [], isFetching } = useGetAllQuery();
const { data = [], isFetching } = useGetCountryQuery();
const { data = [], isFetching } = useGetRegionQuery(option);
return (
<>
{data.map((country) => (
<Link onClick={() => {}} key={country.name.official} to={`/details/${country.name.official}`} >
<CountryCard
key={country.name.official}
name={country.name.official}
capital={country.capital}
region={country.region}
population={country.population}
flag={country.flags.svg}
/>
</Link>
))}
</>
);
};
export default CountryList;
As you can see I have to destructure "data" and "isFetching" for all three hooks which is how I know functions, What is an alternative way for me to use all three API hooks so i can use it on the same component being "CountryCard" I want to display?
For one, you could decide to just not destructure.
const allResult = useGetAllQuery();
const countriesResult = useGetCountryQuery();
const regionResult= useGetRegionQuery(option);
return (
<>
{countriesResult.data?.map((country) => (
Or, you rename things while destructuring:
const { data: all = [], isFetching: allFetching } = useGetAllQuery();
const { data: countries = [], isFetching: countryFetching } = useGetCountryQuery();
const { data: regions = [], isFetching: regionFetching } = useGetRegionQuery(option);

Change a checkbox checked state from a useEffect hook in React?

The value of context.number is 1. I want to set the input checked value to true based off the context.number. So if context.number is 1, the checkbox is checked for Module 1, showing that Module 1 is completed. I can only change the input value from the onChange event though, so i assume i need a useEffect hook so it does it automatically when context.number changes?
import React, { useState, useContext, useEffect } from "react";
import { Link } from "react-router-dom";
import { AppContext } from "../context/AppContext";
export default function Menu() {
const context = useContext(AppContext);
//Determine the modules
const modules = [
{
title: `Introduction`,
subtitle: "Lesson 1",
},
{
title: `Overview`,
subtitle: "Lesson 2",
}
];
//Create a checked state for each module
const [checkedState, setCheckedState] = useState(new Array(modules.length).fill(false));
//Change checked value method
const handleOnChange = (position) => {
//map through checked states array, if position === mapped item index, flip the value
const updatedCheckedState = checkedState.map((item, index) => (index === position ?
!item : item));
//set that items state to the new value in the array
setCheckedState(updatedCheckedState);
};
return (
<>
<div>
{modules.map((module, index) => (
<div className={styles.menuCard}>
<Link key={index} to={`/Module/${index + 1}`}>
<h2>{module.title}</h2>
<p>{module.subtitle}</p>
</Link>
<input
id={`check${index}`}
type="checkbox"
onChange={() => handleOnChange(index)}
checked={checkedState[index]}
/>
</div>
))}
</div>
</>
);
}
I put this is my context file and was able to get it to work. This is a poorly worded question I realize but found my answer none the less. useEffect wasn't the problem, it was that each checkbox was only saving local state so if i rendered another page the checkboxes went back to being unchecked.
import React, { createContext, useState } from "react";
const AppContext = createContext();
function AppProvider(props) {
//Determine the modules
const modules = [
{
title: `Introduction`,
subtitle: "Lesson 1",
},
{
title: `Overview`,
subtitle: "Lesson 2",
}
];
//set module number state
const [number, setNumber] = useState();
//Create a checked state for each module
const [checkedState, setCheckedState] = useState(new Array(modules.length).fill(false));
//change module number method
function completionHandler(value) {
setNumber(value);
}
//Change checked value method
function handleChange(position) {
const updatedCheckedState = checkedState.map((item, index) => (index == position ? !item : item));
setCheckedState(updatedCheckedState);
}
//export method and state value
const value = {
number: number,
modules: modules,
checkedState: checkedState,
handleChange: handleChange,
completionHandler: completionHandler,
};
return <AppContext.Provider value={value}>{props.children}</AppContext.Provider>;
}
export { AppContext, AppProvider };

Testing React Context update

I have been struggling to test a React Context update for a while. I can predefine the value of the Context.Provider however, the solution is not ideal because an update to the context which is supposed to happen within component utilising the context is not actually happening.
When I test this manually the text 'Account name: abc' changes to 'Account name: New account name' but not in the test. The context value remains the same.
The reason I predefine the value is because the component relies on a fetch response from a parent and I am unit testing <ImportAccounts /> only.
In a test I am rendering a component with predefined value
test('accountName value is updated on click', () => {
const { getByText, getByLabelText, getByRole } = render(
<ImportAccountsContext.Provider value={{ accountName: { value: 'abc', setValue: jest.fn() }}}>
<ImportAccounts />
</ImportAccountsContext.Provider>,
);
expect(getByText('Account name: abc')).toBeInTheDocument();
const input = getByLabelText('Account name');
fireEvent.change(input, { target: { value: 'New account name' } });
fireEvent.click(getByRole('button', { name: 'Update' }));
expect(getByText('Account name: New account name')).toBeInTheDocument();
});
Here's my context
import React, { createContext, useContext, useState, useCallback, Dispatch, SetStateAction } from 'react';
export interface StateVariable<T> {
value: T;
setValue: Dispatch<SetStateAction<T>>;
}
export interface ImportAccountsState {
accountName: StateVariable<string>;
}
export const ImportAccountsContext = createContext<ImportAccountsState>(
{} as ImportAccountsState,
);
export const ImportAccountsProvider = ({ children }: { children: React.ReactNode }) => {
const [accountName, setAccountName] = useState('');
const initialState: ImportAccountsState = {
accountName: {
value: accountName,
setValue: setAccountName,
},
};
return (
<ImportAccountsContext.Provider value={initialState}>
{children}
</ImportAccountsContext.Provider>
);
};
export const useImportAccountsContext = () => {
return useContext<ImportAccountsState>(ImportAccountsContext);
};
Import Accounts is as simple as
export const ImportAccounts = () => {
const { accountName } = useImportAccountsContext();
const [newAccountName, setNewAccountName] = useState(accountName.value);
const handleAccountNameChange = () => {
accountName.setValue(newAccountName);
};
return (
<>
<h1>Account name: {accountName.value}</h1>
<label htmlFor="accountName">Account name</label>
<input
value={newAccountName}
onChange={e => setNewAccountName(e.target.value)}
id="accountName"
/>
<button
type="button"
onClick={handleAccountNameChange}>
Update
</button>
</>
);
}
How can I test that accountName has actually updated?
If we don't need the default value for the ImportAccounts provider then we make the test pass easily. ImportAccountsProvider manages the state of the accountName within itself. In that provider, we are passing the accountName state of type ImportAccountsState to all our children through the context provider.
Now coming to your problem,
const { getByText, getByLabelText, getByRole } = render(
<ImportAccountsContext.Provider value={{ accountName: { value: 'abc', setValue: jest.fn() }}}>
<ImportAccounts />
</ImportAccountsContext.Provider>,
);
Here, the value: 'abc' is not a state value, it's simply a string constant 'abc' which will never be going to change. This is something that we should note. We must pass the state value to the context provider if we want to share the value with the children which is not going to be constant in the entire react lifecycle.
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
test('should update the context with existing provider', () => {
render(
<ImportAccountsProvider>
<ImportAccounts />
</ImportAccountsProvider>
);
// some constant hoisted to make it clean code
const accountInput = screen.getByRole('textbox', { name: /account name/i });
const accountInputValue = 'subrato patnaik';
expect(accountInput).toHaveAttribute('value', '');
// do some changes
userEvent.type(accountInput, accountInputValue);
//validate "changes"
expect(screen.getByDisplayValue(accountInputValue)).toBeTruthy();
expect(accountInput).toHaveAttribute('value', accountInputValue);
// update context
userEvent.click(screen.getByRole('button', { name: /update/i }));
// validate update
expect(screen.getByRole('heading')).toHaveTextContent(/subrato/i);
screen.debug();
});
Inside the ImportAccountsProvider we can do the fetch call and set the accountName state to the response of the fetch call.
export const ImportAccountsProvider = ({ children }: { children: React.ReactNode }) => {
const [accountName, setAccountName] = useState('');
useEffect(() => {
// do the fetch call here and update the accountName state accordingly
});
const initialState: ImportAccountsState = {
accountName: {
value: accountName,
setValue: setAccountName,
},
};
return (
<ImportAccountsContext.Provider value={initialState}>
{children}
</ImportAccountsContext.Provider>
);
};
export const useImportAccountsContext = () => {
return useContext<ImportAccountsState>(ImportAccountsContext);
};

Transferring data from one state object to another while using React context in a Next.js app

I'm building a pretty simple restaurant website using React and Next.js. I have a home page and an 'order' page, which renders a menu from a state object ('menuitems'). I want the user to be able to add items to their 'cart', which is another state object. So ultimately I'm transferring data from the static 'menuitems' state to the 'cart.'
What I can't figure out is how I can update the 'cart' state from my 'order' page.
I've set up a context for the app going off of this guide. I've been able to successfully access the menu using a call to the custom hook 'useAppContext()' but I no longer have access to the updater functions provided through useState() or useContext() calls I previously used when I built something similar with everything in a single file (you can see in my code below where I invoke the now-undefined setCartItem() function).
How can I update the 'cartitems' state from inside of my 'order' component?
File where I have my context object:
import { createContext, useContext } from "react";
const AppContext = createContext();
export function AppWrapper({ children }) {
const state = {
menuitems: [
{
title: "Spagett",
description: "Our finest spagett made with homemade meatballs and sauce.",
},
{
title: "Sandwich",
description: "You gotta try this sandwich",
},
{
title: "BFB",
description: "Watch out for your toilet after this one bro",
},
],
cartitems: []
}
return (
<AppContext.Provider
value={state}
>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
My _app.js file:
import "../styles/globals.css";
import { AppWrapper } from "./context/state";
function MyApp({ Component, pageProps }) {
return (
<AppWrapper>
<Component {...pageProps} />
</AppWrapper>
);
}
export default MyApp;
Finally, my 'order' component, where I am trying to update the 'cartitems' state:
import Link from "next/link";
import { useAppContext } from "./context/state";
//IMPORT CONTEXT
const OrderPage = () => {
const { menuitems } = useAppContext();
const { cartitems } = useAppContext();
const renderedMenu = menuitems.map((item) => (
<div key={item.name} className="order-item">
<h4>{item.title}</h4>
<p>{item.description}</p>
<button onClick={() => setCartItem([...cartitems, item.title])}>
Add Item
</button>
</div>
));
return (
<div>
<Link href="/">
<a>Go home</a>
</Link>
<div>{renderedMenu}</div>
</div>
);
};
export default OrderPage;
Create a state in your provider, then pass setCartItems into your context.
export function AppWrapper({ children }) {
const [ cartItems, setCardItems ] = useState([])
const state = {
menuitems: [
{
title: "Spagett",
description: "Our finest spagett made with homemade meatballs and sauce.",
},
{
title: "Sandwich",
description: "You gotta try this sandwich",
},
{
title: "BFB",
description: "Watch out for your toilet after this one bro",
},
],
cartitems: []
}
return (
<AppContext.Provider
value={{ state, cartItems, setCardItems }}
>
{children}
</AppContext.Provider>
);
}
You can then use it this way.
const { state, cartItems, setCartitems } = useAppContext();

Resources