I everyone. I try to post simple login form with a custom fetch hook. The login works fine if I authenticate well at the first time. But in error case if I try to revalidate the form the component seems to be unmounted (hooks clean up function is called) and i can't refetch my api. Why this component is unmounted i don't understand ? Thanks for your help
signin.ts (the code has been simplified)
import React, { useState } from 'react'
import useFetch from '../shared/hooks/useFetch'
const Signin: React.FunctionComponent = () => {
const [postData, setPostData] = useState({
url: "",
options: {}
})
type ResponseT = {
id: string,
firstname: string,
lastname: string,
roles: string[],
accessToken: string
} & string
const { data, error } = useFetch<ResponseT>(postData.url, postData.options)
const handleSubmit = (e: any) => {
if (e) e.preventDefault()
setPostData({
url: `${process.env.REACT_APP_API_HOST}/signin`,
options: {
method: "POST",
body: JSON.stringify({
email: "testt#test.fr",
password: "bla"
})
}
})
}
return (
<form>
<div>
<div className="form-group">
<button onClick={handleSubmit} className="btn btn-primary btn-block">Sign In</button>
{error && <p>{error}</p>}
{data === '' && <p style={{ height: '100px', backgroundColor: 'red' }}>Loading...</p>}
</div>
</div>
</form>
)
}
export default Signin
useFetch.ts
import { useEffect, useReducer, useRef } from 'react'
interface State<T> {
data?: T
error?: Error
}
type Cache<T> = { [url: string]: T }
type Action<T> =
| { type: 'loading' }
| { type: 'fetched'; payload: T }
| { type: 'error'; payload: Error }
function useNiceFetch<T = unknown>(url?: string, options?: RequestInit): State<T> {
const cache = useRef<Cache<T>>({})
const cancelRequest = useRef<boolean>(false)
const initialState: State<T> = {
error: undefined,
data: undefined,
}
const fetchReducer = (state: State<T>, action: Action<T>): State<T | any> => {
switch (action.type) {
case 'loading':
return { ...initialState, data: '' }
case 'fetched':
return { ...initialState, data: action.payload }
case 'error':
return { ...initialState, error: action.payload }
default:
return state
}
}
const [state, dispatch] = useReducer(fetchReducer, initialState)
useEffect(() => {
if (!url) return
const fetchData = async () => {
if (cancelRequest.current) return
dispatch({ type: 'loading' })
if (cache.current[url]) {
dispatch({ type: 'fetched', payload: cache.current[url] })
return
}
try {
const response = await fetch(url, options) as any
const responseJson = await response.json()
if (!response.ok) {
throw responseJson
}
cache.current[url] = responseJson as T
dispatch({ type: 'fetched', payload: responseJson })
} catch (error) {
if (cancelRequest.current) return
dispatch({ type: 'error', payload: error as Error })
}
}
fetchData()
return () => {
cancelRequest.current = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url, options])
return state
}
export default useFetch
Related
I'm using react with Context Api and react hook form to add a new post. When I submit a post, the new post is in the last position but when I refresh the page, the post goes to the top position. I want the new post to be in the top position immediately. Do you know what I did wrong, please?
PostsContext.tsx
export interface Post {
id: number
content: string
thumbnail: {
url: string
}
created_at: string
updated_at: string
userId: number
}
export interface PostsState {
posts: Post[]
}
export type PostsAction =
| { type: 'SET_POSTS'; payload: Post[] }
| { type: 'ADD_POST'; payload: Post }
const initialState: PostsState = {
posts: [],
}
const reducer = (state: PostsState, action: PostsAction) => {
switch (action.type) {
case 'SET_POSTS':
return { posts: action.payload }
case 'ADD_POST':
return { posts: [...state.posts, action.payload] }
default:
return state
}
}
export const PostsContext = createContext<{
state: PostsState
dispatch: React.Dispatch<PostsAction>
}>({
state: initialState,
dispatch: () => null,
})
export const PostsProvider = ({ children }: PropsWithChildren<{}>) => {
const [state, dispatch] = useReducer(reducer, initialState)
return <PostsContext.Provider value={{ state, dispatch }}>{children}</PostsContext.Provider>
}
Feed.tsx
const Feed = () => {
const { state, dispatch } = usePostsContext()
useEffect(() => {
const fetchPosts = async () => {
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/posts`, {
credentials: 'include',
})
const data = await res.json()
if (res.ok) {
dispatch({ type: 'SET_POSTS', payload: data })
}
}
fetchPosts()
}, [])
return (
<div className="container mx-auto">
<PostForm />
{state.posts.length < 1 ? (
<div className="mt-4 p-8 text-center border border-gray-200 rounded-lg">
<h2 className="text-2xl font-medium">There's nothing here...</h2>
<p className="mt-4 text-sm text-gray-500">
Created posts will appear here, try creating one!
</p>
</div>
) : (
state.posts.map((post) => <Posts key={post.id} post={post} />)
)}
</div>
)
}
export default Feed
PostForm.tsx
const PostForm = () => {
const { dispatch } = usePostsContext()
const {
register,
handleSubmit,
control,
watch,
reset,
formState: { isSubmitSuccessful, errors },
} = useForm<FormInput>({
defaultValues: {
content: '',
},
resolver: yupResolver(postSchema),
})
const selectedFile = watch('thumbnailFile')
const onSubmit: SubmitHandler<FormInput> = async (data) => {
const formData = new FormData()
formData.append('content', data.content)
formData.append('thumbnailFile', data.thumbnailFile[0])
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/posts`, {
method: 'POST',
credentials: 'include',
body: formData,
})
const post = await response.json()
if (response.ok) {
console.log('post created', post)
dispatch({ type: 'ADD_POST', payload: post })
reset()
}
}
return (
<MyForm />
)
}
export default PostForm
Looks like your ADD_POST reducer is adding a new post to the end of the list since you are placing the action payload after the destructing of your old posts.
To place a post at the beginning of the list you need to place the action payload before destructing the old list of posts.
i.e.,
case 'ADD_POST':
return { posts: [action.payload, ...state.posts] }
i made a custom hook for fetching data the problem is when i use <React.StrictMode> the fetch singal for aborting gets fire but some how it works if i remove strict mode
this is the fetch hook
import { useEffect, useReducer } from 'react';
import { ApiResponse } from '../interfaces/ApiResponse';
const initialState: ApiResponse = {
loading: false,
data: null,
error: null,
};
type Action =
| { type: 'start' }
| { type: 'error'; payload: Error }
| { type: 'success'; payload: JSON };
const reducer = (state: ApiResponse, action: Action) => {
switch (action.type) {
case 'start':
return {
loading: true,
data: null,
error: null,
};
case 'success':
return {
loading: false,
data: action.payload,
error: null,
};
case 'error':
return {
loading: false,
data: null,
error: action.payload,
};
default:
return state;
}
};
export const useFetch = (url: string): ApiResponse => {
const [response, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
const controller: AbortController = new AbortController();
const signal: AbortSignal = controller.signal;
const fetchData = async () => {
dispatch({ type: 'start' });
try {
const response: Response = await fetch(url, { signal: signal });
if (response.ok) {
const json = await response.json();
dispatch({
type: 'success',
payload: json,
});
} else {
dispatch({
type: 'error',
payload: new Error(response.statusText),
});
}
} catch (error: any) {
dispatch({
type: 'error',
payload: new Error(error),
});
}
};
fetchData();
return () => {
controller.abort();
};
}, [url]);
return response;
};
when i call this hook in one of my components like this:
const Grid = () => {
const response = useFetch(`${BASE_API_URL}/games`);
useEffect(() => {
console.log(response);
}, [response]);
return (
<div className='grid__wrapper'>
<div className='grid__content'>
{response.loading && <h4>Loading...</h4>}
<h4>helo</h4>
</div>
</div>
);
};
export default Grid;
the response.loading is never set to true and i can see an abort error in the logs but if i remove strict mode it works fine
I am using axios to fetch data and then want to render the component. For that, I have loading which gets set to true when fetching and to false when all the data has come.
But I am getting error. Is there a way to trigger useEffect before rendering of component ?
Following is the code:
GithubReducer.js
import {
SET_USERS,
CLEAR_USERS,
SET_LOADING,
SET_USER,
CLEAR_USER,
} from "../types";
const GithubReducer = (state, action) => {
switch (action.type) {
case SET_USERS: {
return { ...state, users: action.payload };
}
case CLEAR_USERS: {
return { ...state, users: [] };
}
case SET_LOADING: {
return { ...state, loading: action.payload };
}
case SET_USER: {
return { ...state, user: action.payload };
}
case CLEAR_USER: {
return { ...state, user: null };
}
default:
return state;
}
};
export default GithubReducer;
GithubState.js
import React, { useReducer } from "react";
import axios from "axios";
import {
SET_USERS,
CLEAR_USERS,
SET_LOADING,
SET_USER,
CLEAR_USER,
} from "../types";
import GithubReducer from "./GithubReducer";
import GithubContext from "./GithubContext";
const GithubState = (props) => {
const initialState = {
loading: false,
users: [],
user: null,
};
const [state, dispatch] = useReducer(GithubReducer, initialState);
const setLoading = (val) => dispatch({ type: SET_LOADING, payload: val });
const getGithubUsers = async () => {
setLoading(true);
dispatch({ type: CLEAR_USER });
const res = await axios.get(`https://api.github.com/users`);
dispatch({
type: SET_USERS,
payload: res.data,
});
setLoading(false);
};
const clearUsers = () => {
dispatch({ type: CLEAR_USERS });
};
const searchUsersWithName = async (username) => {
setLoading(true);
const res = await axios.get(
`https://api.github.com/search/users?q=${username}`
);
dispatch({ type: SET_USERS, payload: res.data.items });
setLoading(false);
};
const fetchGithubUserProfile = async (username) => {
setLoading(true);
const res = await axios.get(`https://api.github.com/users/${username}`);
dispatch({ type: SET_USER, payload: res.data });
setLoading(false);
};
return (
<GithubContext.Provider
value={{
getGithubUsers,
clearUsers,
searchUsersWithName,
fetchGithubUserProfile,
users: state.users,
loading: state.loading,
user: state.user,
}}
>
{props.children}
</GithubContext.Provider>
);
};
export default GithubState;
User.js
import React, { useContext, useEffect } from "react";
import { useParams } from "react-router-dom";
import GithubContext from "../../context/github/GithubContext";
import Spinner from "../layout/Spinner";
const User = () => {
const { fetchGithubUserProfile, user, loading } = useContext(GithubContext);
const { username } = useParams();
useEffect(() => {
fetchGithubUserProfile(username);
// eslint-disable-next-line
}, []);
if (loading) return <Spinner />;
else {
return (
<div className="user">
<button>Go Back</button>
<section className="about">{user.login}</section>
</div>
);
}
};
export default User;
And, this is the error I am getting:
TypeError: Cannot read properties of null (reading 'login')
User
D:/anubh/Desktop/github-finder/src/components/users/User.js:21
18 | return (
19 | <div className="user">
20 | <button>Go Back</button>
> 21 | <section className="about">{user.login}</section>
| ^ 22 | </div>
23 | );
24 | }
Very simple. You can't. useEffect runs after componentDidMount, or after the JSX has been rendered.
Here is a solution. Render your JSX conditionally depending on state, which you can set once your data is retrieved.
return (
{data ? <MyComponent /> : null}
)
I have a question on handling errors in createAsyncThunk with TypeScript.
I declared returned type and params type with generics. However I tried with handling erros typing I ended up just using 'any'.
Here's api/todosApi.ts...
import axios from 'axios';
export const todosApi = {
getTodosById
}
// https://jsonplaceholder.typicode.com/todos/5
function getTodosById(id: number) {
return instance.get(`/todos/${id}`);
}
// -- Axios
const instance = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com'
})
instance.interceptors.response.use(response => {
return response;
}, function (error) {
if (error.response.status === 404) {
return { status: error.response.status };
}
return Promise.reject(error.response);
});
function bearerAuth(token: string) {
return `Bearer ${token}`
}
Here's todosActions.ts
import { createAsyncThunk } from '#reduxjs/toolkit'
import { todosApi } from '../../api/todosApi'
export const fetchTodosById = createAsyncThunk<
{
userId: number;
id: number;
title: string;
completed: boolean;
},
{ id: number }
>('todos/getTodosbyId', async (data, { rejectWithValue }) => {
try {
const response = await (await todosApi.getTodosById(data.id)).data
return response
// typescript infer error type as 'unknown'.
} catch (error: any) {
return rejectWithValue(error.response.data)
}
})
And this is todosSlice.ts
import { createSlice } from '#reduxjs/toolkit'
import { fetchTodosById } from './todosActions'
interface todosState {
todos: {
userId: number;
id: number;
title: string;
completed: boolean;
} | null,
todosLoading: boolean;
todosError: any | null; // I end up with using any
}
const initialState: todosState = {
todos: null,
todosLoading: false,
todosError: null
}
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
},
extraReducers: (builder) => {
builder
.addCase(fetchTodosById.pending, (state) => {
state.todosLoading = true
state.todosError = null
})
.addCase(fetchTodosById.fulfilled, (state, action) => {
state.todosLoading = false
state.todos = action.payload
})
.addCase(fetchTodosById.rejected, (state, action) => {
state.todosLoading = false
state.todosError = action.error
})
}
})
export default todosSlice.reducer;
In addition, it seems my code doesn't catch 4xx errors. Is it becasue I didn't throw an error in getTodosById in todosApi?
I don't have much experience with TypeScript so please bear with my ignorance.
UPDATE: I managed to handle errors not using 'any' type, but I don't know if I'm doing it right.
//todosActions..
export const fetchTodosById = createAsyncThunk<
{
userId: number;
id: number;
title: string;
completed: boolean;
},
number
>('todos/getTodosbyId', async (id, { rejectWithValue }) => {
const response = await todosApi.getTodosById(id);
if (response.status !== 200) {
return rejectWithValue(response)
}
return response.data
})
// initialState...
todosError: SerializedError | null;
This is described in the Usage with TypeScript documentation page:
const fetchUserById = createAsyncThunk<
// Return type of the payload creator
MyData,
// First argument to the payload creator
number,
{
// Optional fields for defining thunkApi field types
rejectValue: YourAxiosErrorType
}
>('users/fetchById', async (userId, thunkApi) => {
// ...
})
I have wriiten the below code in which the city the alert function initially works fine when a wrong city name or no city name is entered. But after the Weather details are displayed here again when I click on submit then it re renders the previous state and new one and gives both result.
Code:
import React, { FC, useState, FormEvent } from "react";
import { useDispatch } from "react-redux";
import { Header, Input, Button } from "../style";
import {
getWeather,
setLoading
} from "../../store/actions/WeatherAction/weatherActions";
import { setAlert } from "../../store/actions/AlertAction/alertActions";
interface SearchProps {
title: string;
}
const Search: FC<SearchProps> = ({ title }) => {
const dispatch = useDispatch();
const [city, setCity] = useState("");
const changeHandler = (e: FormEvent<HTMLInputElement>) => {
setCity(e.currentTarget.value);
};
const submitHandler = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
dispatch(setLoading());
dispatch(getWeather(city));
setCity("");
};
return (
<>
<Header>
{title}
<form onSubmit={submitHandler}>
<Input
type="text"
placeholder="Enter city name"
value={city}
onChange={changeHandler}
/>
<br />
<Button>Search</Button>
</form>
</Header>
</>
);
};
export default Search;
weatherAction.ts
import { ThunkAction } from "redux-thunk";
import { RootState } from "../..";
import {
WeatherAction,
WeatherData,
WeatherError,
GET_WEATHER,
SET_LOADING,
SET_ERROR
} from "../../types";
export const getWeather = (
city: string
): ThunkAction<void, RootState, null, WeatherAction> => {
return async (dispatch) => {
try {
const res = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=3020950b62d2fb178d82816bad24dc76`
);
if (!res.ok) {
const resData: WeatherError = await res.json();
throw new Error(resData.message);
}
const resData: WeatherData = await res.json();
dispatch({
type: GET_WEATHER,
payload: resData
});
} catch (err) {
dispatch({
type: SET_ERROR,
payload: err.message
});
}
};
};
export const setLoading = (): WeatherAction => {
return {
type: SET_LOADING
};
};
export const setError = (): WeatherAction => {
return {
type: SET_ERROR,
payload: ""
};
};
weatherReducer
import {
WeatherState,
WeatherAction,
GET_WEATHER,
SET_LOADING,
SET_ERROR
} from "../../types";
const initialState: WeatherState = {
data: null,
loading: false,
error: ""
};
export default (state = initialState, action: WeatherAction): WeatherState => {
switch (action.type) {
case GET_WEATHER:
return {
data: action.payload,
loading: false,
error: ""
};
case SET_LOADING:
return {
...state,
loading: true
};
case SET_ERROR:
return {
...state,
error: action.payload,
loading: false
};
default:
return state;
}
};
The problem is that your reducer does not clear the weather data when processing a SET_ERROR action. If you want to clear the weather data when you receive an error, you should set data back to null like this:
case SET_ERROR:
return {
data: null,
error: action.payload,
loading: false
};