I'm currently working on an app that fetches data from an API and displays it on the front-end. I've got the data coming up correctly when I print it to the console (so I know fetching the data isn't an issue) and I want to be able to use this data globally across different components. I'm aiming to use createContext to do this but haven't figured it out yet. Here's my code so far:
useFetch.ts
export const useFetch = (url: string) => {
const [fetchedData, setFetchedData] = useState(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
getAllFetchedData()
}, []);
const getAllFetchedData = async () => {
await axios.get(url).then((response) => {
const data = response.data;
setFetchedData(data)
setLoaded(true)
})
.catch(error => console.error(`Error: ${error}`));
}
return [loaded, fetchedData];
}
DataProvider.tsx
type PostProviderProps = {
children: React.ReactNode
}
export default function DataProvider({ children }: PostProviderProps) {
const [loaded, data] = useFetch(`https://jsonplaceholder.typicode.com/posts`)
return (
<PostContext.Provider value = {[loaded, data]}>
{children}
</PostContext.Provider>
)
}
DataContext.ts
type PostContextType = {
userId: number;
id: number;
title: string;
body: string;
}
const PostContext = createContext<PostContextType>({} as PostContextType)
export default PostContext
If anybody could help it'd be really appreciated 🙂
The PostContext is typed to be an object with userId, id, title, and body properties, but the value you are trying to provide is an array of the loaded and data values returned from the useFetch hook. It appears that you are fetching an array of posts.
I suggest the following updates to the code.
First, update the useFetch hook to take a generic type for the fetched return value.
const useFetch = <T extends unknown>(url: string): [boolean, T | null] => {
const [fetchedData, setFetchedData] = useState<T | null>(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const getAllFetchedData = async () => {
await axios
.get(url)
.then((response) => {
const data = response.data;
setFetchedData(data);
setLoaded(true);
})
.catch((error) => console.error(`Error: ${error}`));
};
getAllFetchedData();
}, []);
return [loaded, fetchedData];
};
Update the context to use a better interface that aligns with (1) what you are fetching and (2) what you want to provide to the app as a context value.
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
interface PostContextType {
loaded: boolean;
posts: Post[] | null;
}
const PostContext = createContext<PostContextType>({
loaded: false,
posts: []
});
Create the DataProvider component and a custom hook to access the context.
const usePosts = () => React.useContext(PostContext);
function DataProvider({ children }: React.PropsWithChildren<{}>) {
const [loaded, data] = useFetch<Post[]>(
`https://jsonplaceholder.typicode.com/posts`
);
const value = {
loaded,
posts: data
};
return (
<PostContext.Provider value={value}>
{children}
</PostContext.Provider>
);
}
Wrap the app code with the provider.
function App() {
return (
<DataProvider>
....
</DataProvider>
);
}
Consume the context in a descendent component.
const MyComponent = () => {
const { loaded, posts } = usePosts();
return loaded ? (
posts?.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<h3>{post.userId}</h3>
<p>{post.body}</p>
</div>
))
) : (
<>"Loading"</>
);
};
Related
Description
I'm creating a state management tool for a small project, using mainly useSyncExternalStore from React, inspired by this video from Jack Herrington https://www.youtube.com/watch?v=ZKlXqrcBx88&ab_channel=JackHerrington.
But, I'm running into a pattern that doesn't look right, which is having to use 2 providers, one to create the state, and the other to initialise it.
The gist of the problem:
I have a property sessionId coming from an HTTP request. Saving it in my store wasn't an issue.
However, once I have a sessionId then all of my POST requests done with notifyBackend should have this sessionId in the request body. And I was able to achieve this requirement using the pattern above, but I don't like it.
Any idea how to make it better ?
Code
CreateStore.jsx (Not important, just providing the code in case)
export default function createStore(initialState) {
function useStoreData(): {
const store = useRef(initialState);
const subscribers = useRef(new Set());
return {
get: useCallback(() => store.current, []),
set: useCallback((value) => {
store.current = { ...store.current, ...value };
subscribers.current.forEach((callback) => callback());
}, []),
subscribe: useCallback((callback) => {
subscribers.current.add(callback);
return () => subscribers.current.delete(callback);
}, []),
};
}
const StoreContext = createContext(null);
function StoreProvider({ children }) {
return (
<StoreContext.Provider value={useStoreData()}>
{children}
</StoreContext.Provider>
);
}
function useStore(selector) {
const store = useContext(StoreContext);
const state = useSyncExternalStore(
store.subscribe,
() => selector(store.get()),
() => selector(initialState),
);
// [value, appendToStore]
return [state, store.set];
}
return {
StoreProvider,
useStore,
};
}
Creating the state
export const { StoreProvider, useStore } = createStore({
sessionId: "INITIAL",
notifyBackend: () => { },
});
index.jsx
<Router>
<StoreProvider>
<InitialisationProvider>
<App />
</InitialisationProvider>
</StoreProvider>
</Router
InitialisationContext.jsx
const InitialisationContext = createContext({});
export const InitializationProvider = ({ children }) {
const [sessionId, appendToStore] = useStore(store => store.session);
const notifyBackend = async({ data }) => {
const _data = {
...data,
sessionId,
};
try {
const result = await fetchPOST(data);
if (result.sessionId) {
appendToStore({ sessionId: result.sessionId });
} else if (result.otherProp) {
appendToStore({ otherProp: result.otherProp });
}
} catch (e) { }
};
useEffect(() => {
appendToStore({ notifyBackend });
}, [sessionId]);
return (
<InitialisationContext.Provider value={{}}>
{children}
</InitialisationContext.Provider>
);
}
I just tried out Zustand, and it's very similar to what I'm trying to achieve.
Feels like I'm trying to reinvent the wheel.
With Zustand:
main-store.js
import create from 'zustand';
export const useMainStore = create((set, get) => ({
sessionId: 'INITIAL',
otherProp: '',
notifyBackend: async ({ data }) => {
const _data = {
...data,
sessionId: get().sessionId,
};
try {
const result = await fetchPOST(data);
if (result.sessionId) {
set({ sessionId: result.sessionId });
} else if (result.otherProp) {
set({ otherProp: result.otherProp });
}
} catch (e) { }
},
}));
SomeComponent.jsx
export const SomeComponent() {
const sessionId = useMainStore(state => state.sessionId);
const notifyBackend = useMainStore(state => state.notifyBackend);
useEffect(() => {
if (sessionId === 'INITIAL') {
notifyBackend();
}
}, [sessionId]);
return <h1>Foo</h1>
};
This answer focuses on OPs approach to createStore(). After reading the question a few more times, I think there are bigger issues. I'll try to get to these and then extend the answer.
Your approach is too complicated.
First, the store is no hook! It lives completely outside of react. useSyncExternalStore and the two methods subscribe and getSnapshot are what integrates the store into react.
And as the store lives outside of react, you don't need a Context at all.
Just do const whatever = useSyncExternalStore(myStore.subscribe, myStore.getSnapshot);
Here my version of minimal createStore() basically a global/shared useState()
export function createStore(initialValue) {
// subscription
const listeners = new Set();
const subscribe = (callback) => {
listeners.add(callback);
return () => listeners.delete(callback);
}
const dispatch = () => {
for (const callback of listeners) callback();
}
// value management
let value = typeof initialValue === "function" ?
initialValue() :
initialValue;
// this is what useStore() will return.
const getSnapshot = () => [value, setState];
// the same logic as in `setState(newValue)` or `setState(prev => newValue)`
const setState = (arg) => {
let prev = value;
value = typeof arg === "function" ? arg(prev) : arg;
if (value !== prev) dispatch(); // only notify listener on actual change.
}
// returning just a custom hook
return () => useSyncExternalStore(subscribe, getSnapshot);
}
And the usage
export const useMyCustomStore = createStore({});
// ...
const [value, setValue] = useMyCustomStore();
I am trying make an "easy" weather app exercise, just get data from api and render it. I am using "google api map" to get the location from a post code to a latitude and longitud parameters so I can use those numbers and pass it to "open weather map" api to get the weather for that location.
It is working but with bugs...
First I used redux for "location" and "weather". Redux was working but useSelector() wasnt displaying the data properly.
Then I decide to make it easy, on "search" component I am calling an api an getting the location I need, I am storing it with redux and it is working, on "weatherFullDispaly" component I am calling an api for the "weather" details and just pass it as props for the children to render the data but they are not getting it.
The thing is, while the app is running, when I put a post code I get an error because the children are not receiving the data but, if I comment out the children on the parent component and then comment in again, all the data print perfect.
Any help please???
const WeatherFullDisplay = () => {
const [weatherDetails, setWeatherDetails] = useState();
const currentLocation = useSelector(getLocationData);
useEffect(() => {
getWeatherDetails();
}, []);
const getWeatherDetails = async () => {
const API_KEY = process.env.REACT_APP_OPEN_WEATHER_MAP_API_KEY;
const { lat, lng } = await currentLocation.results[0].geometry.location;
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/onecall?lat=${lat}&lon=${lng}&exclude=minutely&units=metric&appid=${API_KEY}`
);
setWeatherDetails(response.data);
};
return (
<div className="weather-full-display-details">
<WeatherNow weatherDetails={weatherDetails} />
<HourlyWeather weatherDetails={weatherDetails} />
<FiveDaysWeather weatherDetails={weatherDetails} />
</div>
);
};
const FiveDaysWeather = ({ weatherDetails }) => {
const displayDailyWeather = () => {
const daysToShow = [
weatherDetails.daily[1],
weatherDetails.daily[2],
weatherDetails.daily[3],
weatherDetails.daily[4],
weatherDetails.daily[5],
];
return daysToShow.map((day, i) => {
return (
<WeatherSingleCard
key={i}
typeOfCard="daily"
weekDay={moment(day.dt * 1000).format("dddd")}
icon={day.weather[0].icon}
weather={day.weather[0].main}
temp={day.temp.day}
/>
);
});
};
return (
<div className="day-single-cards">{displayDailyWeather()}</div>
);
};
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
locationDetails: "",
};
const locationSlice = createSlice({
name: "location",
initialState,
reducers: {
setLocation: (state, action) => {
state.locationDetails = action.payload;
},
cleanLocation: (state) => {
state.locationDetails = ""
}
},
});
export const { setLocation, cleanLocation } = locationSlice.actions;
export const getLocationData = (state) => state.location.locationDetails;
export default locationSlice.reducer;
const SearchBar = () => {
const [postCode, setPostCode] = useState();
const [locationDetails, setLocationDetails] = useState();
const navigate = useNavigate();
const dispatch = useDispatch();
useEffect(() => {
getLocationDetails();
}, [postCode]);
const getLocationDetails = async () => {
const response = await axios.get(
"https://maps.googleapis.com/maps/api/geocode/json",
{
params: {
components: `country:ES|postal_code:${postCode}`,
region: "ES",
key: process.env.REACT_APP_GOOGLE_API_KEY,
},
}
);
setLocationDetails(response.data);
};
const handleSubmit = (e) => {
e.preventDefault();
dispatch(setLocation(locationDetails));
navigate("/detail-weather");
};
const handleChange = (e) => {
setPostCode(e.target.value);
};
I got the error:
Property 'title' does not exist on type 'never'.
..... console.log(blog.title);
But at the console of Browser I am able to see the "blog.title".
Here a screenshot from Browser
In this file I use the console.log and recieve the error:
const BlogDetails = () => {
const { id } = useParams<{ id: string }>();
const {
data: blog,
error,
isPending,
} = useFetch("http://localhost:8500/blogs/" + id);
console.log(blog&& blog.title);
return (
<div className="blog-details">
{isPending && <div>loading...</div>}
{error && <div>{error}</div>}
{blog && (
<article>
<h2>BlogDetails</h2>
</article>
)}
</div>
);
};
export default BlogDetails;
I use custom hook to fetch the Data:
import { useState, useEffect } from "react";
const useFetch = (url: string) => {
const [data, setData] = useState(null);
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortCont = new AbortController();
setTimeout(() => {
fetch(url, { signal: abortCont.signal })
.then((res) => {
if (!res.ok) {
throw Error("could not fetch the data for that resource");
}
return res.json();
})
.then((data) => {
setIsPending(false);
setData(data);
setError(null);
})
.catch((err) => {
if (err.name === "AbortError") {
console.log("fetch aborted");
} else {
setIsPending(false);
setError(err.message);
}
});
}, 1000);
return () => abortCont.abort();
}, [url]);
return { data, isPending, error };
};
export default useFetch;
Typescript can't infer the types from an api call, so you'll need to provide them explicitly.
However, since useFetch is a generic function, you'll need to add a type that fits the call, and pass it to the internal useState. In addition, the initial value of useState is null, so we should also consider that.
We can add a generic type to useFetch - <T>, and type useState<T | null>() to allow for the intended data type, and null:
export const useFetch = <T>(url: string) => {
const [data, setData] = useState<T | null>(null);
To use it, you'll need to pass an explicit type to useFetch. For example:
interface Data {
title: string;
body: string;
author: string;
id: number;
}
const { data: blog, error, isPending } = useFetch<Data>(
'http://localhost:8500/blogs/' + id
);
console.log(blog && blog.title); // or just console.log(blog?.title) with optional chaining
Whenever you call useFetch you should provide it with the data type that fits the current response. For example:
useFetch<string[]>( // the result is an array of strings
useFetch<Data[]>( // the result is an array of data objects
useFetch<number>( // the result is a number
I am developing a hook in which I can pass a function that makes a web request and it returns isLoading, data and error.
const [isLoading, data, error] = useApi(getMovie, idMovie, someAction);
basically I have a hook (useApi) that receives 3 parameters:
a function that resolves a web request
the parameters of this web request
and finally the call to a callback.
I use it like this:
const idMovie = { _id: "3" };
// callback function
const someAction = (data: unknown) => {
// do something
return data;
};
const [isLoading, data, error] = useApi(getMovie, idMovie, someAction);
useApi.tsx
import { AxiosPromise, AxiosResponse } from 'axios';
import { useState, useEffect } from 'react';
const useApi = (
apiFunction: (params: unknown) => AxiosPromise,
params = {},
callback: (data: unknown) => void
): [boolean, unknown, null | string] => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<null | string>(null);
useEffect(() => {
apiFunction(params)
.then(({ data }: AxiosResponse) => {
setData(data);
setIsLoading(false);
if (callback) {
callback(data);
}
})
.catch(() => {
setError('Something went wrong');
setIsLoading(false);
});
}, []); //apiFunction, params, callback]
return [isLoading, data, error];
};
export default useApi;
getMovie corresponds to a function that solves a web request
import axios from "axios";
type getMovieParams = { _id: string };
const BASE_URL = "https://jsonplaceholder.typicode.com/todos/";
const getMovie = (params: getMovieParams): Promise<unknown> => {
if (params) {
const { _id } = params;
if (_id) {
const url = `${BASE_URL}/${_id}`;
return axios.get(url);
}
}
throw new Error("Must provide a query");
};
export default getMovie;
the code that calls this hook would look like this:
import "./styles.css";
import useApi from "./useApi";
import getMovie from "./api";
interface Movie {
userId: number;
id: number;
title: string;
completed: boolean;
}
export default function App() {
const idMovie = { _id: "3" };
// callback function
const someAction = (data: unknown) => {
// do something
return data;
};
const [isLoading, data, error] = useApi(getMovie, idMovie, someAction);
const dataResponse = error ? [] : data; //type Movie
console.log(dataResponse);
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
{error && <div>{error}</div>}
-- Get Data Movie 1--
<p>{dataResponse.title}</p>
</div>
);
}
I am getting an typing error on this line:
<p>{dataResponse.title}</p>
this is because:
const dataResponse = error ? [] : data;
data is of type unknown, this is because it could be any type of data, but I want to specify in this case that the data type is 'Movie', the idea is to reuse this hook in another component and be able to say the type of data that will be obtained when this hook returns data.
this is my live code:
https://codesandbox.io/s/vigilant-swartz-iozn4
The reason for the creation of this question is to ask for your kind help to solve the typescript problems that are marked with a red line. (file app.tsx)
How can fix it? thanks
It is possible to use generics to capture types related to the args you're passing to a function. For instance, in the screenshot you have, TS complaints about the params, we can declare a generic to capture the params type
function useApi<Params>(
apiFunction: (params: Params) => AxiosPromise,
params: Params,
callback: (data: unknown) => void
)
with this, our second hook argument will be typed as whatever type the apiFunction asks for in its arg.
we do the same to type the data we return to the callback
function useApi<Params, Return>(
apiFunction: (params: Params) => Promise<AxiosResponse<Return>>,
params: Params,
callback: (data: Return) => void
)
I got the return type of the apiFunction by inspecting the types for the axios.get call
get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
as you can see, in the end it returns a Promise<AxiosResponse<T>>. We intercept that T with our generic typing and name it Return
now we can use these typings in the return types and body of the hook as well
): [boolean, Return | null, null | string] {
const [data, setData] = useState<Return | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<null | string>(null);
useEffect(() => {
apiFunction(params)
.then(({ data }) => {
setData(data);
setIsLoading(false);
if (callback) {
callback(data);
}
})
.catch(() => {
setError("Something went wrong");
setIsLoading(false);
});
}, []); //apiFunction, params, callback]
return [isLoading, data, error];
}
as your Movie model is specific for the getMovie call, you can move that interface there and type the axios.get call
interface Movie {
userId: number;
id: number;
title: string;
completed: boolean;
}
const getMovie = (params: getMovieParams) => {
if (params) {
const { _id } = params;
if (_id) {
const url = `${BASE_URL}/${_id}`;
return axios.get<Movie>(url);
}
}
throw new Error("Must provide a query");
};
with all these additions you'll observe some warnings on the code when you're using the hook that will force you to implement the rendering taking into account all possible values, I rewrote it like this
...
const [isLoading, dataResponse, error] = useApi(
getMovie,
idMovie,
someAction
);
if (isLoading) {
return <div>Loading...</div>;
}
if (error) return <div>error</div>;
return <div>{dataResponse?.title}</div>;
...
https://codesandbox.io/s/generic-hook-arg-types-s8vtt
I found that a good solution is to use generics. With this solution you should be able to provide any type parameter and as long as your API data fits those type requirements it should work.
API.tsx
import axios from "axios";
type getMovieParams = { _id: string };
const BASE_URL = "https://jsonplaceholder.typicode.com/todos/";
async function getMovie<T>(params: getMovieParams): Promise<T> {
if (params) {
const { _id } = params;
if (_id) {
const url = `${BASE_URL}/${_id}`;
const data = await axios.get<T>(url);
const movies = await data.data;
console.log(movies);
return new Promise<T>((res, rej) => {
if (movies) {
res(movies);
} else {
rej("No movies found");
}
});
}
}
throw new Error("Must provide a query");
}
export default getMovie;
In the API the only change is the return type of the function, instead of an AxiosResponse we return a Promise of type T.
App.tsx
import "./styles.css";
import useApi from "./useApi";
import getMovie from "./api";
export interface Movie {
userId: number;
id: number;
title: string;
completed: boolean;
}
export default function App() {
const idMovie = { _id: "3" };
// callback function
const someAction = (data: unknown) => {
// do something
return data;
};
const [isLoading, data, error] = useApi<Movie>(getMovie, idMovie, someAction);
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
{error && <div>{error}</div>}
-- Get Data Movie 1--
<p>{data ? data.id : ''}</p>
</div>
);
}
useApi.tsx
import { AxiosPromise, AxiosResponse } from "axios";
import { useState, useEffect } from "react";
function useApi<T>(
apiFunction: (params: any) => Promise<T>,
params = {},
callback: (data: T) => void
): [boolean, T, null | string] {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<null | string>(null);
useEffect(() => {
apiFunction(params)
.then((res) => {
setData(res);
setIsLoading(false);
})
.catch((e) => {
setError(e.toString());
setIsLoading(false);
});
}, [apiFunction, params, callback]); //apiFunction, params, callback]
return [isLoading, data as T, error];
}
export default useApi;
Setting useAPI to generic and it’s data to generic means we can handle use this hook to find any data that can interface with a given type T, including arrays of T.
You’ll notice the last line ‘data as T’ this will not cause an error later because you never interact with data at a potential null or undefined state, the data is null when the error exists.
codesandbox
I was working on a shopping website this morning, using React Typescript and Context API ...
for the products, I used an open API from the web to retrieve some data (for the test), and I used that data as an initial state for my [products, setProducts] ...
when I console.log(products) my products state inside the Context File, I do get the data but when I console.log it in the App after using the provider I get an empty Array...
I have no idea why that's happening...
thanks for your help
Context API File
import * as React from 'react';
import { useQuery } from "react-query";
type IProductContext = [IProductItem[] | undefined, React.Dispatch<React.SetStateAction<IProductItem[] | undefined>>];
export const ProductContext = React.createContext<IProductContext>([[], () => null]);
const getProducts = async (): Promise<IProductItem[]> =>
await (await fetch("https://fakestoreapi.com/products")).json();
const ProductProvider: React.FC<{}> = ({children}: { children?: React.ReactNode }) => {
//Retrieve the data and status using UseQuery
const {data, isLoading, error} = useQuery<IProductItem[]>('products', getProducts);
const [products, setProducts] = React.useState<IProductItem[] | undefined>(data || undefined);
console.log(products)
return (
<ProductContext.Provider value={[products, setProducts]}>
{children}
</ProductContext.Provider>
);
};
export default ProductProvider;
export function useProducts(){
const context = React.useContext(ProductContext);
if(!context) throw new Error('useProducts must be inside a ProductProvider.');
return context;
}
Types File
interface IProductItem{
id: number
title: string
description: string;
category: string;
image: string;
price: number;
quantity: number;
}
type ProductType = {
items: IProductItem[]
saveItem: (item: ICartItem) => void
updateItem: (id: number) => void
removeItem: (id: number) => void
};
My App.tsx file
import React, { useContext } from 'react';
import ProductProvider, {useProducts} from './infoContext/ProductContext';
function App() {
const [products, setProducts] = useProducts()
return (
<ProductProvider>
<div className="App">
<div>
test
{
console.log(products)
}
</div>
</div>
</ProductProvider>
);
}
export default App;
React.useState will use the passed value only the first time the component is rendered. To alter the value stored in the state you need to use the useProducts function.
In your case you should use a useEffect in the provider that updates the products when the data is changed.
Something like
const ProductProvider: React.FC<{}> = ({children}: { children?: React.ReactNode }) => {
//Retrieve the data and status using UseQuery
const {data, isLoading, error} = useQuery<IProductItem[]>('products', getProducts);
const [products, setProducts] = React.useState<IProductItem[] | undefined>(data || undefined);
console.log(products)
React.useEffect( ()=> {
setProducts(data);
}, [data]);
return (
<ProductContext.Provider value={[products, setProducts]}>
{children}
</ProductContext.Provider>
);
};