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>
);
};
Related
I created a context search in my application, where I have an array called "searchPosts". My goal is to send an object from a component into this array in context and thus be able to use it in other components. I would like to create a global state where my object is stored
context
import { createContext } from "react";
export type SearchContextType = {
searchPost: (text: string) => void;
};
export const SearchContext = createContext<SearchContextType>(null!);
provider
import React, { useState } from "react";
import { SearchContext } from "./SearchContext"
export const SearchProvider = ({ children }: { children: JSX.Element }) => {
const [searchPosts, setSearchPosts] = useState([]);
const searchPost = (text: string) => {
}
return (
<SearchContext.Provider value={{searchPost}}>
{ children }
</SearchContext.Provider>
);
}
I created this search function because in theory it should be a function for me to add the item to the array, but I don't know how I could do that.
This is the state that I have in my component called "searchPosts" that I get the object that I would like to pass to my global array. I want to pass the information from this array in this component to my global array in context
const navigate = useNavigate();
const api = useApi();
const [searchText, setSearchText] = useState('');
const [searchPost, setSearchPost] = useState([]);
const handleSearch = async () => {
const posts = await api.getAllPosts();
const mapPosts = posts.filter(post => post.title.toLowerCase().includes(searchText));
setSearchPost(mapPosts);
}
In the component searchPosts, try to import SearchContext and import searchPost function from context component using useContext hook.
import {SearchContext} from './SearchContext'
const {searchPost} = useContext(SearchContext);;
Now, inside your handleSearch function, pass the mapPosts array to searchPost function that you imported from useContext hook, like this:
const handleSearch = async () => {
const posts = await api.getAllPosts();
const mapPosts = posts.filter(post => post.title.toLowerCase().includes(searchText));
setSearchPost(mapPosts);
searchPost(mapPosts);
}
Now, inside your searchPost function inside your provider component, add following code:
const searchPost = (posts: any[]) => {
setSearchPosts(prev => {
return [...prev, ...posts];
})
}
Add the searchPost[] to SearchContextType
import { createContext } from "react";
export type SearchContextType = {
searchPost: (text: string) => void;
searchResult: string[];
};
export const SearchContext = createContext<SearchContextType>(null!);
Create a reducer to manage your dispatch
//const SEARCH_POST = "SEARCH_POST"; in constants.ts
import { SEARCH_POST } from 'constants';
// reducer
export const reducer = (state, action) => {
switch (action.type) {
case SEARCH_POST: {
return { ...state, searchResult: action.value };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
Create the provider
interface InitState {
searchResult: string[];
}
export const SearchProvider = ({ children }: { children: JSX.Element }) => {
const initialState: InitState = {
searchResult: [],
};
const [controller, dispatch] = useReducer(reducer, initialState);
const value = useMemo(() => [controller, dispatch], [controller, dispatch]);
return <SearchContext.Provider value={value}>{children}</SearchContext.Provider>;
};
export const searchPost = (dispatch, value) => dispatch({ type: SEARCH_POST, value });
Now create a custom hook to access the context
export const useSearchState = () => {
const context = useContext(SearchContext);
if (!context) {
throw new Error(
"useSearchState should be used inside the SearchProvider."
);
}
return context;
};
In your component, you can use the above to access the state.
const [controller, dispatch] = useSearchState();
const {searchResult} = controller;
// to update the post you can call searchPost
// import it from search provider
searchPost(dispatch, posts)
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"</>
);
};
I'm new to the typescript world and I'm creating a new project in nextjs using typescript and wanted to add auth functionality with it so I'm using createContext I previously used javascript where we don't need to define default value while creating context. these are some errors I'm getting in my vscode please if someone mentors me with code it would be great.
full AuthContext.tsx
import { createContext, useEffect, useState } from "react";
import React from 'react'
import { setCookie } from 'cookies-next';
import { useRouter } from "next/router";
const AuthContext = createContext();
export function AuthProvider({ children }: any) {
const [user, setUser] = useState(null)
const [error, setError] = useState(null)
const router = useRouter()
useEffect(() => {
checkUserLoggedIn();
}, []);
const logout = async () => {
}
const login = async (vals: object) => {
}
const register = async (vals: object) => {
}
const checkUserLoggedIn = async () => {
}
return (
<AuthContext.Provider value={{ user, error, register, login, logout }}>
{children}
</AuthContext.Provider>
)
}
export default AuthContext
my login.tsx
import AuthContext from '../context/AuthContext'
import { useContext, useEffect } from 'react';
const Login = () => {
const { login, error } = useContext(AuthContext)
useEffect(() => error && toast.error(error))
And for the user variable, I only need
this object
{
username:"ansh3453",
id:43
}
well you need a type you can put this on your types, I think here you need add your error and user object here:
export type CustomizationProps = {
user: any,
error: any,
logout: () => void;
login: (vals: object) => void;
register: (vals: object) => void;
checkUserLoggedIn : () => void;
};
after need create the context, you can add your user and error here too, before create the context:
const initialState: CustomizationProps = {
user: any,
error: any,
logout: () => {},
login: () => {},
register: () => {},
checkUserLoggedIn : () => {}
};
const ConfigContext = createContext(initialState);
now for your function
type ConfigProviderProps = {
children: React.ReactNode;
};
export function AuthProvider({ children }: ConfigProviderProps) {
const [user, setUser] = useState(null)
const [error, setError] = useState(null)
const router = useRouter()
useEffect(() => {
checkUserLoggedIn();
}, []);
const logout = async () => {
}
const login = async (vals: object) => {
}
const register = async (vals: object) => {
}
const checkUserLoggedIn = async () => {
}
return (
<AuthContext.Provider value={{ user, error, register, login, logout }}>
{children}
</AuthContext.Provider>
)
}
export default AuthContext
I guess this is an example not easy to write but you should add your error and user to the type
The error is really self-descriptive:
Expected 1 arguments, but got 0.
An argument for 'defaultValue' was not provided
createContext requires an argument that sets the default value of the Context if it ever imported outside the boundaries of a Context Provider.
Take a look at the React documentation for createContext: https://reactjs.org/docs/context.html#reactcreatecontext
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
Trying a couple of things out with React, NextJS and Typescript. I am running into a problem where I keep getting the "x does not exist on type 'boolean | any[]'. Property 'x' does not exist on type 'false'.ts(2339)" error. I just don't know where to put the interface to show it what types this is using. Can anyone help me out? Here is the offending code!
Articles.js
import React, { FunctionComponent } from 'react'; // we need this to make JSX compile
import useFetch from '../hooks/fetch'
type artProps = {
title: string,
author: string,
date: number
}
type article = {
title: string,
author: string,
date: number
}
export const Articles: FunctionComponent<{artProps}> = () => {
const url = 'http://localhost:4000/kb'
const data = useFetch(url)
console.log(data)
return (
<div>
{data.map(articles) => {
return (<div key={article.id}>
{console.log(article)}
<h2 key={article.id}>{article.title} </h2>
<p key={article.id}>{article.body}</p>
</div>
)
})}
{/* {artProps.map(artProp => {
return (
<div>
<h2>{artProp.title}</h2>
<p>{artProp.author}</p>
</div>
)
})
} */}
</div>
)
}
export default Articles
The hook
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const useFetch = (url) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
console.log('fetch')
axios.get(url)
.then(res => {
if (res.data) {
setData(res.data)
setLoading(false)
}
})
}
, []);
return [data, loading];
};
export default useFetch
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const useFetch = <T>(url: string): [T, boolean] => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
console.log('fetch')
axios.get(url)
.then((res: any) => {
if (res.data) {
setData(res.data)
setLoading(false)
}
})
}
, []);
return [data, loading];
};
export default useFetch
Now add this to the parent component...
useFetch<ArrayTypeHere>(url)
now .map will infer the correct type and no need for annotations or type casting.