how to setInterval on redux action without re-rendering the whole app - reactjs

I have an app with redux and redux toolkit.
I try to have a background call to refresh my notifications in the background but every time it is called the whole app gets refreshed.
In my Notification slice I have the following createAsyncThunk:
// Thunk is an async middleware for handling reducers
export const reloadNotifications = createAsyncThunk(
'notifications/reload',
async (userToken: string): Promise<Notification[]> => {
try {
const req = await axios.post(process.env.REACT_APP_GRAPHQL_ENDPOINT as string, {
query: myNotifications()
}, { headers: { "Authorization": `Bearer ${userToken}` } })
// Check data exists before pushing
if (req?.data?.data?.myNotification) {
return req.data.data?.myNotification as Notification[]
} else {
return []
}
} catch (error) {
return []
}
}
)
In my NotificationHeader component I have this:
export const NotificationHeader: React.FC<any> = () => {
const notifications = useSelector(s => s.notifications.notifications);
const [seenNotification] = useMutation(SEEN_NOTIFICATION);
const [location, setLocation] = useLocation();
const dispatch = useDispatch();
const auth = useSelector(s => s.auth);
const handleClick = (notification: Notification) => {
seenNotification({ variables: { notificationID: notification.id } }).then(async ({ data }) => {
setLocation(getUrlNotification(notification, auth.user.id))
}).catch((e: any) => {
setLocation(getUrlNotification(notification, auth.user.id))
})
}
useEffect(() => {
const timer = setTimeout(
() => {
// Only get notifications if i'm logged in
if (auth.isLogged) {
dispatch(reloadNotifications(auth.token))
}
}, 3000);
// This handles componentUnmount to clear the timer
return () => clearTimeout(timer);
});
return (
<Menu placement="bottom-start">
<MenuButton marginRight="10px" variant="secondaryAction" bg="brand.orange" color="brand.purple" as={Button}>
{(notifications && notifications.length > 0 && notifications.length < 10 &&
<SmallBadge content={notifications.length} />
)}
{(notifications && notifications.length > 9 &&
<SmallBadge content="9+" />
)}
<FontAwesomeIcon icon={faBell} />
</MenuButton>
<MenuList commandSpacing="sm" bg="brand.purple" color="brand.orange">
{(notifications && notifications.length > 0) ?
notifications.map(notif => (
<MenuItem key={`notif-${notif.id}`} maxH={20} _focus={{ bg: "brand.orange", color: "brand.purple" }} className="notificationItem">
{(!notif.isSeen) ? <Badge marginRight={2} size="sm" colorScheme="green">NEW</Badge> : undefined}
<Link href="#" onClick={() => handleClick(notif)}>{notif.title}</Link><Text marginLeft={4} marginRight={2} textAlign="right" flex="1" color="brand.gray" as="i" fontSize="xs">{moment(notif.createdAt).format(`DD MMM YYYY`)}</Text>
</MenuItem>
))
: (
<MenuItem isFocusable={false} textAlign="center" maxH={20} _focus={{ bg: "brand.orange", color: "brand.purple" }}>
You have no new notifications
</MenuItem>
)}
</MenuList>
</Menu >
);
}
However with this the interval causes a full refresh of the app even non child components.
I have also tried to add the following middleware to cause the notification interval to be triggered but this caused the full app to crash
export const updateNotificationsMiddleware: Middleware = api => next => action => {
const updateNotifications = async () => {
const { auth } = api.getState() as State;
api.dispatch({
type: 'notifications/reload',
payload: auth.token
});
setTimeout(updateNotifications, 3000);
};
updateNotifications();
return next(action);
};
How can I have a simple API call with redux that will refresh my state every x second without causing a full app refresh.
I have also tried the following from the answer below:
let initialized = false
export const updateNotificationsMiddleware: Middleware = api => next => action => {
const updateNotifications = async () => {
console.log('in middleware')
const { auth } = api.getState() as State;
api.dispatch({
type: 'notifications/reload',
payload: auth.token
});
setTimeout(updateNotifications, 3000);
};
if (!initialized){
initialized = true
updateNotifications();
}
return next(action);
};
I have then updated my thunk to reflect the following:
// Thunk is an async middleware for handling reducers
export const reloadNotifications = createAsyncThunk(
'notifications/reload',
async (userToken: string): Promise<Notification[]> => {
console.log('in action')
try {
const req = await axios.post(process.env.REACT_APP_GRAPHQL_ENDPOINT as string, {
query: myNotifications()
}, { headers: { "Authorization": `Bearer ${userToken}` } })
// Check data exists before pushing
if (req?.data?.data?.myNotification) {
return req.data.data?.myNotification as Notification[]
} else {
return []
}
} catch (error) {
return []
}
}
)
The middleware console.log is indeed shown every 3 second which is awesome but the action is still never called. The console.log does not appear once and the network request also does not get triggered.

Gave your middleware a re-read. You add a timer on every action happening, which probably causes your problem. I think getting it down to doing that only once should solve your problem:
let initialized = false
export const updateNotificationsMiddleware: Middleware = api => next => action => {
const updateNotifications = async () => {
const { auth } = api.getState() as State;
api.dispatch({
type: 'notifications/reload',
payload: auth.token
});
setTimeout(updateNotifications, 3000);
};
if (!initialized){
initialized = true
updateNotifications();
}
return next(action);
};

Related

Why is my local storage item removed after a short period automatically?

I have a custom popup modal for user authentication in react.js and it redirects to the main page after successful authentication.
Meanwhile, it should save a local storage item named accessToken in JWT.
But what I've noticed is that the accessToken is saved in local storage sometimes, but sometimes the accessToken is removed automatically even it is saved beforehand.
In other words, it doesn't work every time, just works sometimes.
Here's my login popup.
import React, {
useEffect,
useRef,
useState
} from 'react';
import { number } from "yup";
const createPopup = ({
url,
title,
height,
width,
}) => {
const left = window.screenX + (window.outerWidth - width) / 2;
const top = window.screenY + (window.outerHeight - height) / 2.5;
return window.open(
url,
title,
`width=${width}, height=${height}, left=${left}, top=${top}`,
);
};
const LoginPopup = ({
title = '',
width = 500,
height = 500,
url,
children,
onCode,
onClose,
}) => {
const [externalWindow, setExternalWindow] = useState(Window | null);
const intervalRef = useRef(number);
const clearTimer = () => {
window.clearInterval(intervalRef.current);
};
const onContainerClick = () => {
setExternalWindow(createPopup({
url, title, width, height,
}));
};
useEffect(() => {
if (externalWindow) {
intervalRef.current = window.setInterval(() => {
try {
const currentUrl = externalWindow.location.href;
const params = new URL(currentUrl).searchParams;
const code = params.get('code');
if (!code) {
return;
}
onCode(code, params);
clearTimer();
externalWindow.close();
} catch (error) {
// eslint-ignore-line
} finally {
if (!externalWindow || externalWindow.closed) {
onClose();
clearTimer();
}
}
}, 200);
}
return () => {
if (externalWindow) externalWindow.close();
if (onClose) onClose();
};
}, [externalWindow]);
return (
// eslint-disable-next-line
<div onClick={() => onContainerClick()}>
{children}
</div>
);
};
export default LoginPopup;
onCode is from Context
const setSession = (key, value) => {
if (key && value) {
localStorage.setItem(key, value);
} else {
localStorage.removeItem(key);
}
};
...
export const AuthProvider = ({ children }) => {
const login = async (accessToken, params) => {
setSession('accessToken', accessToken);
try {
const response = await axios.get(`${domain}/api/v1/account`);
const user = response?.data;
dispatch({
type: 'LOGIN',
payload: {
user
}
});
} catch (err) {
console.error(err);
}
};
useEffect(() => {
const initialise = async () => {
...
};
initialise();
}, []);
return (
<AuthContext.Provider
value={{
...state,
...
login,
logout
}}
>
{children}
</AuthContext.Provider>
);
};
export const AuthContext;
Not sure why it is removed immediately after the popup is closed even it is saved in the local storage but a very short period.
I am pretty sure the back-end has the correct redirection url so it should work correctly.

React limits the number of renders to prevent an infinite loop...Too many re-renders

How would I avoid the infinite loop issue?
I'm getting an error while rendering the following component:
Too many re-renders. React limits the number of renders to prevent an infinite loop.?
TeamContent.js re-renders multiple times, how can I set an initial render on load?
Error given
TeamContent.js
import { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
fetchTeamPlayers,
fetchUpcomingGames,
fetchPreviousGames,
fetchLiveGames,
} from "../../../data/UserInfo/infoActions";
import TeamPlayers from "./TeamPlayers";
import TeamNext from "./TeamNext";
import TeamPrevious from "./TeamPrevious";
import LiveEvent from "./Live.js/LiveEvent";
function TeamContent(props) {
console.log("test");
let containsLiveGame = false;
const dispatch = useDispatch();
const liveGames = useSelector((store) => store.userInfo.live.games.all);
const status = useSelector((store) => store.userInfo.playersLoadStatus);
const UpcomingGamesstatus = useSelector(
(store) => store.userInfo.upcomingGamesStatus
);
const previousGamesStatus = useSelector(
(store) => store.userInfo.previousGamesStatus
);
const liveStatus = useSelector((store) => store.userInfo.live.games.status);
liveGames.map((game) => {
const verifyHomeTeam = +game.idHomeTeam === +props.teamID;
const verifyAwayTeam = +game.idAwayTeam === +props.teamID;
if (verifyAwayTeam || verifyHomeTeam) {
containsLiveGame = true;
}
});
// -----> request team data
useEffect(() => {
dispatch(fetchTeamPlayers(props.teamID));
dispatch(fetchUpcomingGames(props.teamID));
dispatch(fetchPreviousGames(props.teamID));
dispatch(fetchLiveGames());
}, [dispatch, props.teamID]);
useEffect(() => {
dispatch(fetchLiveGames());
const interval = setInterval(() => {
dispatch(fetchLiveGames());
}, 30000);
return () => clearInterval(interval);
}, [dispatch]);
return (
<div className="teamDash">
<div className="dashLeft">
<div
className="dashLeftHead"
style={{
backgroundImage: `url(${props.stadiumImg})`,
}}
>
<div className="dashLeftHeadAbs"></div>
<div className="dashLeftHeadIntro">
<span>{props.stadiumName}</span>
<h3>{props.teamName}</h3>
</div>
</div>
{liveStatus !== "error" && containsLiveGame && <LiveEvent />}
{status !== "error" && (
<div className="dashLeftPlayers">
<TeamPlayers />
</div>
)}
<div className="dashLeftDesc">
<p>{props.teamDesc}</p>
</div>
</div>
<div className="dashRight">
{UpcomingGamesstatus === "error" ? (
console.log("unable to load upcoming games")
) : (
<div className="upcomingGames">
<TeamNext id={props.teamID} />
</div>
)}
{previousGamesStatus === "error" ? (
console.log("unable to load previous games")
) : (
<div className="previousGames">
<TeamPrevious />
</div>
)}
</div>
</div>
);
}
export default TeamContent;
infoActions.js
import { API_URL } from "../Api";
import { infoActions } from "./infoSlice";
export function fetchTeams() {
return (dispatch) => {
dispatch(infoActions.loadStatusHandler({ status: "loading" }));
async function getTeams() {
try {
const rq = await fetch(`${API_URL}Lookup_all_teams.php?id=4387`);
const res = await rq.json();
const data = res.teams;
dispatch(infoActions.loadTeamsHandler({ teams: data }));
dispatch(infoActions.loadStatusHandler({ status: "done" }));
} catch (error) {
dispatch(infoActions.loadStatusHandler({ status: "error" }));
}
}
getTeams();
};
}
export function fetchTeamPlayers(id) {
return (dispatch) => {
async function getPlayers() {
dispatch(infoActions.statusPlayersHandler({ status: "loading" }));
try {
const rq = await fetch(`${API_URL}lookup_all_players.php?id=${id}`);
const res = await rq.json();
const data = res.player;
dispatch(infoActions.loadPlayersHandler({ players: data }));
dispatch(infoActions.statusPlayersHandler({ status: "done" }));
} catch (error) {
dispatch(infoActions.statusPlayersHandler({ status: "error" }));
}
}
getPlayers();
};
}
export function fetchUpcomingGames(id) {
return (dispatch) => {
dispatch(infoActions.statusUGHandler({ status: "loading" }));
async function getGames() {
try {
const rq = await fetch(`${API_URL}eventsnext.php?id=${id}`);
const res = await rq.json();
const data = res.events;
dispatch(infoActions.upcomingGamesHandler({ games: data }));
dispatch(infoActions.statusUGHandler({ status: "done" }));
} catch (error) {
dispatch(infoActions.statusUGHandler({ status: "error" }));
}
}
getGames();
};
}
export function fetchPreviousGames(id) {
return (dispatch) => {
dispatch(infoActions.statusPGHandler({ status: "loading" }));
async function getGames() {
try {
const rq = await fetch(`${API_URL}eventslast.php?id=${id}`);
const res = await rq.json();
const data = res.results;
dispatch(infoActions.previousGamesHandler({ games: data }));
dispatch(infoActions.statusPGHandler({ status: "done" }));
} catch (error) {
dispatch(infoActions.statusPGHandler({ status: "error" }));
}
}
getGames();
};
}
export function fetchLiveGames() {
return (dispatch) => {
dispatch(infoActions.statusLiveGames({ status: "loading" }));
async function getGames() {
try {
const rq = await fetch(
`https://www.thesportsdb.com/api/v2/json/40130162/livescore.php?l=4387`
);
const res = await rq.json();
const data = res.events;
dispatch(infoActions.statusLiveGames({ status: "done" }));
dispatch(infoActions.loadLiveGames({ liveGames: data }));
} catch (error) {
dispatch(infoActions.statusLiveGames({ status: "error" }));
}
}
getGames();
};
}
Try remove dispatch from the array you passed to
useEffect(() => {
...
}, [dispatch, props.teamID])
and
useEffect(() => {
...
}, [dispatch])
dispatch is a function, and if you include it into the useEffect listener, the useEffect will trigger on every render

react-redux-spinner won't render

I was searching for a loading spinner for react+redux and came across the react-redux-spinner library. I included it in my project, added the reducer, called [pendingTask]: begin/end in my actions, added the Spinner component to render, but it just won't show at all, even though in the redux logs I can see that pending tasks in the store are incremented and decremented accordingly to the action called. Here is some of my code:
store:
const rootReducer = combineReducers({
pendingTasks: pendingTasksReducer
// other reducers
});
const store = createStore(rootReducer, /* middlewares */);
export default store;
actions
export const fetchData = params => {
const request = params => ({
type: 'FETCH_REQUEST',
[pendingTask]: begin,
payload: { params }
});
const success = data => ({
type: 'FETCH_SUCCESS',
[pendingTask]: end,
payload: { data }
});
const failure = error => ({
type: 'FETCH_FAILURE',
[pendingTask]: end,
payload: { error }
});
return async dispatch => {
dispatch(request(params));
try {
const res = await service.fetchData(params);
dispatch(success(res.data));
return res.data;
} catch (e) {
const msg = e.toString();
dispatch(failure(msg));
return Promise.reject(msg);
}
}
}
page
const Page = props => {
const { data } = props;
useEffect(() => {
async function fetchData(params) {
try {
await props.fetchData(params);
} catch (e) {
console.log(e);
}
}
fetchData(data.params);
}
return (
<div className="wrapper">
{
data.map(({ field1, field2 }, key) => ({
<div>{field1}: {field2}</div>
}));
}
</div>
);
};
const mapStateToProps = state => {
const { data } = state;
return { data };
};
const actionCreators = {
fetchData: actions.fetchData
};
export default connect(mapStateToProps, actionCreators)(Page);
app component
export const App = props => {
return (
<main className="App">
<Spinner config={{ trickeRate: 0.02 }} />
<Page/>
</main>
);
}
I've double-checked that I use the correct names for the store and for the actions, and they do fire up - but the spinner itself never gets rendered on the page at all, even though with each action the pendingTasks value change. What could I possibly do wrong or miss here? Infinitely grateful in advance for pointing out!

To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function

I have this code
import ReactDOM from "react-dom";
import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
function ParamsExample() {
return (
<Router>
<div>
<h2>Accounts</h2>
<Link to="/">Netflix</Link>
<Route path="/" component={Miliko} />
</div>
</Router>
);
}
const Miliko = ({ match }) => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
(async function() {
setIsError(false);
setIsLoading(true);
try {
const Res = await fetch("https://foo0022.firebaseio.com/New.json");
const ResObj = await Res.json();
const ResArr = await Object.values(ResObj).flat();
setData(ResArr);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
})();
console.log(data);
}, [match]);
return <div>{`${isLoading}${isError}`}</div>;
};
function App() {
return (
<div className="App">
<ParamsExample />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I created three links that open the Miliko component. but when I quickly click on the links I get this error:
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
I think the problem is caused by dismount before async call finished.
const useAsync = () => {
const [data, setData] = useState(null)
const mountedRef = useRef(true)
const execute = useCallback(() => {
setLoading(true)
return asyncFunc()
.then(res => {
if (!mountedRef.current) return null
setData(res)
return res
})
}, [])
useEffect(() => {
return () => {
mountedRef.current = false
}
}, [])
}
mountedRef is used here to indicate if the component is still mounted. And if so, continue the async call to update component state, otherwise, skip them.
This should be the main reason to not end up with a memory leak (access cleanedup memory) issue.
Demo
https://codepen.io/windmaomao/pen/jOLaOxO , fetch with useAsync
https://codepen.io/windmaomao/pen/GRvOgoa , manual fetch with useAsync
Update
The above answer leads to the following component that we use inside our team.
/**
* A hook to fetch async data.
* #class useAsync
* #borrows useAsyncObject
* #param {object} _ props
* #param {async} _.asyncFunc Promise like async function
* #param {bool} _.immediate=false Invoke the function immediately
* #param {object} _.funcParams Function initial parameters
* #param {object} _.initialData Initial data
* #returns {useAsyncObject} Async object
* #example
* const { execute, loading, data, error } = useAync({
* asyncFunc: async () => { return 'data' },
* immediate: false,
* funcParams: { data: '1' },
* initialData: 'Hello'
* })
*/
const useAsync = (props = initialProps) => {
const {
asyncFunc, immediate, funcParams, initialData
} = {
...initialProps,
...props
}
const [loading, setLoading] = useState(immediate)
const [data, setData] = useState(initialData)
const [error, setError] = useState(null)
const mountedRef = useRef(true)
const execute = useCallback(params => {
setLoading(true)
return asyncFunc({ ...funcParams, ...params })
.then(res => {
if (!mountedRef.current) return null
setData(res)
setError(null)
setLoading(false)
return res
})
.catch(err => {
if (!mountedRef.current) return null
setError(err)
setLoading(false)
throw err
})
}, [asyncFunc, funcParams])
useEffect(() => {
if (immediate) {
execute(funcParams)
}
return () => {
mountedRef.current = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return {
execute,
loading,
data,
error
}
}
Update 2022
This approach has been adopted in the book https://www.amazon.com/Designing-React-Hooks-Right-Way/dp/1803235950 where this topic has been mentioned in useRef and custom hooks chapters, and more examples are provided there.
useEffect will try to keep communications with your data-fetching procedure even while the component has unmounted. Since this is an anti-pattern and exposes your application to memory leakage, cancelling the subscription to useEffect optimizes your app.
In the simple implementation example below, you'd use a flag (isSubscribed) to determine when to cancel your subscription. At the end of the effect, you'd make a call to clean up.
export const useUserData = () => {
const initialState = {
user: {},
error: null
}
const [state, setState] = useState(initialState);
useEffect(() => {
// clean up controller
let isSubscribed = true;
// Try to communicate with sever API
fetch(SERVER_URI)
.then(response => response.json())
.then(data => isSubscribed ? setState(prevState => ({
...prevState, user: data
})) : null)
.catch(error => {
if (isSubscribed) {
setState(prevState => ({
...prevState,
error
}));
}
})
// cancel subscription to useEffect
return () => (isSubscribed = false)
}, []);
return state
}
You can read up more from this blog juliangaramendy
Without #windmaomao answer, I could spend other hours trying to figure out how to cancel the subscription.
In short, I used two hooks respectively useCallback to memoize function and useEffect to fetch data.
const fetchSpecificItem = useCallback(async ({ itemId }) => {
try {
... fetch data
/*
Before you setState ensure the component is mounted
otherwise, return null and don't allow to unmounted component.
*/
if (!mountedRef.current) return null;
/*
if the component is mounted feel free to setState
*/
} catch (error) {
... handle errors
}
}, [mountedRef]) // add variable as dependency
I used useEffect to fetch data.
I could not call the function inside effect simply because hooks can not be called inside a function.
useEffect(() => {
fetchSpecificItem(input);
return () => {
mountedRef.current = false; // clean up function
};
}, [input, fetchSpecificItem]); // add function as dependency
Thanks, everyone your contribution helped me to learn more about the usage of hooks.
fetchData is an async function which will return a promise. But you have invoked it without resolving it. If you need to do any cleanup at component unmount, return a function inside the effect that has your cleanup code. Try this :
const Miliko = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState('http://hn.algolia.com/api/v1/search?query=redux');
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
(async function() {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
})();
return function() {
/**
* Add cleanup code here
*/
};
}, [url]);
return [{ data, isLoading, isError }, setUrl];
};
I would suggest reading the official docs where it is clearly explained along with some more configurable parameters.
Folowing #Niyongabo solution, the way I ended up that fixed it was:
const mountedRef = useRef(true);
const fetchSpecificItem = useCallback(async () => {
try {
const ref = await db
.collection('redeems')
.where('rewardItem.id', '==', reward.id)
.get();
const data = ref.docs.map(doc => ({ id: doc.id, ...doc.data() }));
if (!mountedRef.current) return null;
setRedeems(data);
setIsFetching(false);
} catch (error) {
console.log(error);
}
}, [mountedRef]);
useEffect(() => {
fetchSpecificItem();
return () => {
mountedRef.current = false;
};
}, [fetchSpecificItem]);
Create a mutable ref object and set it to true, and during clean-up toggle its value, to ensure that the component has been unmouted.
const mountedRef = useRef(true)
useEffect(() => {
// CALL YOUR API OR ASYNC FUNCTION HERE
return () => { mountedRef.current = false }
}, [])
const [getAllJobs, setgetAlljobs] = useState();
useEffect(() => {
let mounted = true;
axios.get('apiUrl')
.then(function (response) {
const jobData = response.data;
if (mounted) {
setgetAlljobs(jobData)
}
})
.catch(function (error) {
console.log(error.message)
})
return () => mounted = false;
}, [])
set a variable mounted to true->
then if it is true, mount the function->
in the bottom you return it to unmount it
My case was pretty different from what this questions wants. Still I got the same error.
My case was because I had a 'list', which was rendered by using .map from array. And I needed to use .shift. (to remove first item in array)
If array had just one item, it was ok, but since it had 2 of them -> the first one got 'deleted/shifted' and because I used key={index} (while index was from .map), it assumed, that the second item, which later was first, was the same component as the shifted item..
React kept info from the first item (they were all nodes) and so, if that second node used useEffect(), React threw error, that the component is already dismounted, because the former node with index 0 and key 0 had the same key 0 as the second component.
The second component correctly used useEffect, but React assumed, that it is called by that former node, which was no longer on the scene -> resulting in error.
I fixed this by adding different key prop value (not index), but some unique string.
you can wrap any action as a callback inside checkUnmount
const useUnmounted = () => {
const mountedRef = useRef(true);
useEffect(
() => () => {
mountedRef.current = false;
},
[],
);
const checkUnmount = useCallback(
(cb = () => {}) => {
try {
if (!mountedRef.current) throw new Error('Component is unmounted');
cb();
} catch (error) {
console.log({ error });
}
},
[mountedRef.current],
);
return [checkUnmount, mountedRef.current];
};
import React, { useCallback, useEffect, useRef, useState } from "react";
import { userLoginSuccessAction } from "../../../redux/user-redux/actionCreator";
import { IUser } from "../../../models/user";
import { Navigate } from "react-router";
import XTextField from "../../../x-lib/x-components/x-form-controls/XTextField";
import { useDispatch } from "react-redux";
interface Props {
onViewChange?: (n: number) => void;
userInit?: (user: IUser) => void;
}
interface State {
email: string;
password: string;
hasError?: boolean;
errorMessage?: string;
}
const initialValue = {
email: "eve.holt#reqres.in",
password: "cityslicka",
errorMessage: "",
};
const LoginView: React.FC<Props> = (props) => {
const { onViewChange } = props;
const [state, setState] = useState(initialValue);
const mountedRef = useRef(true);
const dispatch = useDispatch();
const handleEmailChange = useCallback(
(val: string) => {
setState((state) => ({
...state,
email: val,
}));
},
[state.email]
);
const handlePasswordChange = useCallback(
(val: string) => {
setState((state) => ({
...state,
password: val,
}));
},
[state.password]
);
const onUserClick = useCallback( async () => {
// HTTP Call
const data = {email: state.email , password: state.password}
try{
await dispatch(userLoginSuccessAction(data));
<Navigate to = '/' />
setState( (state)=>({
...state,
email: "",
password: ""
}))
}
catch(err){
setState( (state)=>({
...state,
errorMessage: err as string
}))
}
},[mountedRef] )
useEffect(()=>{
onUserClick();
return ()=> {
mountedRef.current = false;
};
},[onUserClick]);
const Error = (): JSX.Element => {
return (
<div
className="alert alert-danger"
role="alert"
style={{ width: "516px", margin: "20px auto 0 auto" }}
>
{state.errorMessage}
</div>
);
};
return (
<div>
<div>
email: "eve.holt#reqres.in"
<span style={{ paddingRight: "20px" }}></span> password: "cityslicka"{" "}
</div>
{state.errorMessage && <Error />}
<form className="form-inline">
<div className="form-group">
<XTextField
label="email"
placeholder="E-Posta"
value={state.email}
onChange={handleEmailChange}
/>
</div>
<div className="form-group my-sm-3">
<XTextField
type="password"
label="password"
placeholder="Şifre"
value={state.password}
onChange={handlePasswordChange}
/>
</div>
<button type="button" className="btn btn-primary" onClick = {onUserClick} >
Giriş Et
</button>
<a
href="#"
onClick={(e) => {
e.preventDefault();
onViewChange && onViewChange(3);
}}
>
Şifremi Unuttum!
</a>
</form>
<p>
Hələdə üye deyilsiniz? <br />
pulsuz registir olmak üçün
<b>
<u>
<a
style={{ fontSize: "18px" }}
href="#"
onClick={(e) => {
e.preventDefault();
onViewChange && onViewChange(2);
}}
>
kilik edin.
</a>
</u>
</b>
</p>
</div>
);
};
export default LoginView;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
For this problem I used a tricky way
first I deploy a state like this
const [routing,setRouting] = useState(false)
then when my works finished I changed it to true
and change my useEffect like this
useEffect(()=>{
if(routing)
navigation.navigate('AnotherPage')
),[routing]}

Redux Thunk silently fails to update state

A child component has the following button code:
// SelectDonation.js
<button
onClick={(e) => {
e.preventDefault();
this.props.testThunk();
console.log(store.getState());
}}
>Test thunks</button>
this.props.testThunk() does not update the state object. I connected Redux Thunk like so:
// reducer.js
import ReduxThunk from "redux-thunk";
const starting_state = {
log_to_console : 0,
donation_amount : 12,
checkoutStep : 'selectDonation',
};
const reducer = (previous_state = starting_state, action) => {
switch (action.type) {
case 'thunkTest':
return {
...previous_state,
redux_thunk_test_var : action.payload
};
default:
return previous_state;
}
};
export default createStore(reducer, starting_state, applyMiddleware(ReduxThunk));
I expect a new state property redux_thunk_test_var to display in state but it does not onClick. I do see the state variables with initial states in the console though.
Am I not passing down the thunk correctly? Here is App.js
// App.js
{this.props.checkoutStep === checkoutSteps.selectDonation &&
<SelectDonation
dispatch_set_donation_amount = {this.props.dispatch_set_donation_amount}
dispatchChangeCheckoutStep={this.props.dispatchChangeCheckoutStep}
{...this.props}
/>
}
</Modal>
</header>
</div>
);
}
}
const map_state_to_props = (state) => {
return {
log_prop : state.log_to_console,
donation_amount : state.donation_amount,
checkoutStep : state.checkoutStep,
}
};
const map_dispatch_to_props = (dispatch, own_props) => {
return {
dispatch_set_donation_amount : amount => dispatch(set_donation_amount(amount)),
dispatchChangeCheckoutStep : newStep => dispatch(changeCheckoutStep(newStep)),
dispatchUpdateStateData : (stateData, stateVariable) => (dispatch(updateStateData(stateData, stateVariable))),
testThunk
}
};
The action thunk:
// actions.js
export const testThunk = () => {
const testDelay = setTimeout(() => 'Set Timeout done', 2000);
return (dispatch) => {
testDelay.then((data) => dispatch({
type: 'thunkTest',
payload: data })
)
}
};
You need to dispatch the result of the testThunk() action creator. Right now, you're just returning it, and not calling dispatch(testThunk()).
See this gist comparing syntaxes for dispatching to help understand the issue better.
The best way to fix this is to use the "object shorthand" form of mapDispatch. As part of that, I suggest changing the prop names to remove the word "dispatch", which lets you use the simpler ES6 object literal syntax:
const map_dispatch_to_props = {
set_donation_amount,
changeCheckoutStep,
updateStateData,
testThunk,
};
conponentDidMount() {
this.props.testThunk();
}
const map_dispatch_props = {
testThunk
}
//action creator
const fetch = (data) => ({
type: 'thunkTest',
payload: data
})
const fakeFetch = () => new Promise((resolve, reject) => setTimeout(() => resolve('Set Timeout done'), 2000));
export const testThunk = () => (dispatch) => fakeFetch.then(data => dispatch(fetch(data)))

Resources