I know, there is a lot of similar questions although I could not find a solution to my problem. It is the first time I am using Next.js and TypeScrypt.
I am simulating a login with REQRES storing the token in the localStorage as shown below:
import {
FormControl,
FormLabel,
Input,
Heading,
Flex,
Button,
useToast,
} from '#chakra-ui/react';
import { useRouter } from 'next/router';
import { useState } from 'react';
import LStorage from '../utils/localStorage/index';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleEmail = (e: any) => setEmail(e.target.value);
const handlePassword = (e: any) => setPassword(e.target.value);
const router = useRouter();
const toast = useToast();
const success = () => toast({
title: 'Login Successfull',
description: 'You will be redirected now.',
status: 'success',
duration: 1200,
isClosable: true,
});
const failure = (error: string) => toast({
title: 'Login unsuccessfull',
description: error,
status: 'error',
duration: 3000,
isClosable: true,
});
const login = async () => {
const res = await fetch('/api', {
method: 'POST',
body: JSON.stringify({
email,
password,
}),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
});
const json = await res.json();
console.log(json);
if (json.error) {
failure(json.error);
setEmail('');
setPassword('');
} else {
LStorage.set('userToken', json.token);
LStorage.set('userInfo', email);
success();
setTimeout(() => {
router.push('/users');
}, 1500);
}
};
return (<div>
<Flex justifyContent="center">
<Heading my="5">Login</Heading>
</Flex>
<FormControl>
<FormLabel htmlFor="email">Email:</FormLabel>
<Input id="email" type="email" onChange={handleEmail} value={email}/>
<FormLabel htmlFor="password">Password:</FormLabel>
<Input id="password" type="password" onChange={handlePassword} value={password}/>
</FormControl>
<br />
<Button onClick={login}>Login</Button>
</div>);
};
export default Login;
which seem to work fine. Although when trying to get the userInfo from localStorage at the _app.tsx component I get the localStorage not defined, looking for the error I found out the solution below inside the useEffect.
import '../styles/globals.sass';
import { ChakraProvider } from '#chakra-ui/react';
import type { AppProps } from 'next/app';
import { useState, useEffect } from 'react';
import NavBar from '../components/NavBar';
import MainLayout from '../layouts/mainLayout';
import theme from '../styles/theme';
import LStorage from '../utils/localStorage/index';
function MyApp({ Component, pageProps }: AppProps) {
const [userInfo, setUserInfo] = useState<string | null>(null);
const logout = () => {
LStorage.remove('userToken');
LStorage.remove('userInfo');
setUserInfo(null);
};
useEffect(() => {
if (typeof window !== 'undefined') {
if (LStorage.get('userInfo')) {
setUserInfo(LStorage.get('userInfo'));
}
}
console.log('i am here');
}, []);
return (
<ChakraProvider theme={theme}>
<NavBar user={userInfo} logout={logout} />
<MainLayout>
<Component {...pageProps}/>
</MainLayout>
</ChakraProvider>
);
}
export default MyApp;
I understood that the first run will be on the server-side and that is why I got the error, nevertheless, using the useEffect should fix it. The thing is the useEffect does not even run unless I refresh the page... What am I missing??!??
The Login.js is a page inside page folder and the NavBar is a component inside components folder in the root.
import {
Flex, Spacer, Box, Heading, Button,
} from '#chakra-ui/react';
import Link from 'next/link';
import { FC } from 'react';
interface NavBarProps {
user: string | null;
logout: () => void;
}
const NavBar: FC<NavBarProps> = ({ user, logout }: NavBarProps) => (
<Flex bg="black" color="white" p="4">
<Box p="2">
<Heading size="md">
<Link href="/">My Sanjow App</Link>
</Heading>
</Box>
<Spacer />
{user && (
<Box pt="2" pr="4">
<Heading size="md">
<Link href="/users">Users</Link>
</Heading>
</Box>
)}
{user ? (
<Button
variant="ghost"
pr="4"
onClick={logout}
>
<Heading size="md">
<Link href="/">Logout</Link>
</Heading>
</Button>
) : (
<Box pt="2" pr="4">
<Heading size="md">
<Link href="/login">Login</Link>
</Heading>
</Box>
)}
</Flex>
);
export default NavBar;
The utils/localStorage/index
const lsType = {
set: 'setItem',
get: 'getItem',
remove: 'removeItem',
};
const ls = (type: string, itemName: string, itemData?: string): void | string => {
if (typeof window !== 'undefined') {
// eslint-disable-next-line no-undef
const LS = window.localStorage;
if (type === lsType.set && itemData) {
LS[type](itemName, itemData);
return;
}
return LS[type](itemName);
}
};
export default {
set(itemName: string, itemData: string): void {
ls(lsType.set, itemName, itemData);
},
get(itemName: string): string {
return ls(lsType.get, itemName) as string;
},
remove(itemName: string): void {
ls(lsType.remove, itemName);
},
};
You are running the effect only once by passing the [] empty array, pass the props that you expect to change instead of a blank array.
via the docs:
If you want to run an effect and clean it up only once (on mount and >unmount), you can pass an empty array ([]) as a second argument. This tells >React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
Generally speaking, managing userInfo only via localStorage is not a good idea, since you might want to re-render the application when user logs in or logs out, or any other change to the user data (i.e. change the username), and React is not subscribed to changes done to localStorage.
Instead, React has an instrument for runtime data management like that, it's called React Context. That context (let's call it UserContext) could be initializing from localStorage, so that the case when you refresh the page for example. But after that initial bootstrapping all state management should go thru the context. Just don't forget to update both context and localStorage every time you login/logout.
I hope this is just enough to give you the right direction.
Related
I made a simple To Do list application to learn Redux Toolkit and after getting it working without a backend, decided to add a simple Flask server to practice working with Redux Toolkit's createAsyncThunk
I was successfully able to create 3 different async thunks for when a user fetches their to-dos, posts a new to-do, or puts a to-do. Here is my toDoSlice:
import { createSlice, current, createAsyncThunk } from '#reduxjs/toolkit';
import axiosPrivate from '../../api/axios';
const initialState = {
toDos: []
}
export const getToDos = createAsyncThunk('toDo/getToDos', async (user) => {
const response = await axiosPrivate.get(`/to-dos/${user?.user?.firebase_uid}`, { headers: { 'Authorization': user?.idToken }, withCredentials: true });
return response.data
})
export const addToDo = createAsyncThunk('toDo/addToDo', async ({ user, newToDo }) => {
const response = await axiosPrivate.post('/to-dos/new', { userId: user?.user?.id, firebaseUid: user?.user?.firebase_uid, task: newToDo?.task }, { headers: { 'Authorization': user?.idToken }, withCredentials: true });
const data = response.data
return {data, user}
})
export const updateToDo = createAsyncThunk('toDo/updateToDo', async ({ user, toDoId, updateAttributes }) => {
const response = await axiosPrivate.put('/to-dos/update', { userId: user?.user?.id, firebaseUid: user?.user?.firebase_uid, toDoId: toDoId, updateAttributes}, { headers: { 'Authorization': user?.idToken }, withCredentials: true })
const data = response.data
return {data, user}
})
export const toDosSlice = createSlice({
name: 'toDos',
initialState,
reducers: {},
extraReducers(builder) {
builder
.addCase(getToDos.fulfilled, (state, action) => {
state.toDos = action.payload?.toDos
})
.addCase(addToDo.fulfilled, (action) => {
getToDos(action?.payload?.user); // This does not change anything
})
.addCase(updateToDo.fulfilled, (action) => {
getToDos(action?.payload?.user); // This does not change anything
})
}
})
export const { deleteToDo } = toDosSlice.actions;
export const selectToDos = (state) => state.toDos;
export default toDosSlice.reducer;
The problem I am having, is that after a user edits their toDo by marking it complete, I am unsure of where and how to properly fetch the to-dos from the backend. I know that I could technically set the state of the toDo using the redux state and validating if the POST or PUT was successfully, although would like to learn how it is properly done with a GET request thereafter.
My ToDoList component where users can DELETE or PUT their ToDos is as follows:
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Stack from '#mui/material/Stack';
import Divider from '#mui/material/Divider';
import Box from '#mui/material/Box';
import Grid from '#mui/material/Grid';
import List from '#mui/material/List';
import ListItem from '#mui/material/ListItem';
import ListItemButton from '#mui/material/ListItemButton';
import IconButton from '#mui/material/IconButton';
import DeleteIcon from '#mui/icons-material/Delete';
import ListItemIcon from '#mui/material/ListItemIcon';
import ListItemText from '#mui/material/ListItemText';
import Checkbox from '#mui/material/Checkbox';
import Typography from '#mui/material/Typography';
import { getToDos, updateToDo, selectToDos } from '../toDoSlice';
import useAuth from '../../../hooks/useAuth';
function ToDoList() {
const dispatch = useDispatch();
const { user } = useAuth();
const toDos = useSelector(selectToDos);
useEffect(() => {
dispatch(getToDos(user));
}, [dispatch, user])
const handleChange = (toDoId, updateAttributes) => {
dispatch(updateToDo({ user, toDoId, updateAttributes }));
// dispatch(getToDos(user)); // This sometimes causes the GET request to occur before the PUT request, which I don't understand
}
return (
<Stack spacing={2}>
<Divider sx={{ marginTop: 5 }} />
<Grid xs={4} item>
{toDos?.toDos?.length ?
<Box>
<List>
{toDos?.toDos?.map((toDo) => (
<ListItem
key={toDo?.id}
secondaryAction={
<IconButton
edge="end"
onClick={() => handleChange(toDo?.id, { 'deleted': !toDo?.deleted })}
>
<DeleteIcon />
</IconButton>
}
>
<ListItemButton onClick={() => handleChange(toDo?.id, { 'completed': !toDo?.completed })}>
<ListItemIcon>
<Checkbox
name={toDo.task}
checked={toDo.completed}
edge='start'
/>
</ListItemIcon>
<ListItemText
primary={toDo.task}
sx={{ textDecoration: toDo.completed ? 'line-through' : null }}
primaryTypographyProps={{
style: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}
}}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Box>
: <Typography align='center' variant="h6" component="div" mt={2}>No To-Dos</Typography>
}
</Grid >
</Stack >
)
}
export default ToDoList
How do I perform a GET request after the POST or PUT operations? Where should I then put the dispatch(getToDos(user))? The comments in my code show the results of the methods I've already tried
After reading through the Redux-Toolkit docs again and looking through other SO posts, I learned of the proper way to perform asyncThunk calls in series.
Instead of performing them in my slice, I moved the calls to my component and used Promises to execute them. In order to catch the error from the request and have access to it inside the component, you have to use Toolkit's rejectWithValue parameter inside the asyncThunk.
Here's an example:
export const loginUser = createAsyncThunk('user/loginUser', async (loginData, { rejectWithValue }) => {
try {
const response = await axios.post('/login', loginData);
return response.data
} catch (err) {
if (!err.response) {
throw err
}
return rejectWithValue(err.response.data)
}
})
By adding { rejectWithValue } to the parameter and return rejectWithValue(err.response.data), you can access the response in the component, like this:
const handleSubmit = () => {
if (loginData?.email && loginData?.password) {
dispatch(loginUser(loginData))
.unwrap()
.then(() => {
// perform further asyncThunk dispatches here
})
.catch(err => {
console.log(err) // the return rejectWithValue(err.response.data) is sent here
});
}
}
I have a custom hook(useData) that takes query as an argument and then returns data and runtime(time to fetch the data from the API). But I need access to the runtime to my Editor component when I click on the run button. Right now what is happening is when I click on run button(inside Editor.js), it sets the query to the App component using the setter function and then it passes that query to the Table component and then calls the custom hook using that query and then table make use of that data. but I want the runtime in the Editor component, not in the Table component. I know I can call useData hook in the Editor component but my editor component gets rerender every time when we write on the editor, so It calls the useData() hook on each change.
If I create a context using this hook then I can able to access the runtime and data wherever I want.
Anyone, please help me how to convert that to context!
App.js code
import React, { useState } from "react";
import "./assets/output.css";
import Footer from "./components/layouts/Footer";
import Navbar from "./components/layouts/Navbar";
import Sidebar from "./components/layouts/Sidebar";
import TableSection from "./components/table/TableSection";
import Editor from "./components/editor/Editor";
const App = () => {
const [query, setQuery] = useState("");
const [value, setValue] = useState("select * from customers");
return (
<>
<div className="grid grid-cols-layout-desktop grid-rows-layout-desktop bg-gray-600 h-screen">
<Navbar />
<Sidebar setQuery={setQuery} setValue={setValue} />
<Editor setQuery={setQuery} value={value} setValue={setValue} />
{query ? <TableSection query={query} /> : null}
<Footer />
</div>
</>
);
};
export default App;
Editor.js
import React from "react";
import AceEditor from "react-ace";
import "ace-builds/src-min-noconflict/ext-language_tools";
import "ace-builds/src-min-noconflict/mode-mysql";
import "ace-builds/src-noconflict/theme-github";
import useData from "../../hooks/useData";
const Editor = ({ setQuery, value, setValue }) => {
const { runtime } = useData();
const onChange = (newValue) => {
setValue(newValue);
};
const onSubmit = () => {
var Z = value.toLowerCase().slice(value.indexOf("from") + "from".length);
setQuery(Z.split(" ")[1]);
};
return (
<div className="col-start-2 col-end-3 row-start-2 row-end-3 m-6">
<AceEditor
aria-label="query editor input"
mode="mysql"
theme="github"
name={Math.floor(Math.random() * 100000).toString()}
fontSize={16}
minLines={15}
maxLines={10}
width="100%"
showPrintMargin={false}
showGutter
placeholder="Write your Query here..."
editorProps={{ $blockScrolling: true }}
setOptions={{
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true,
}}
value={value}
onChange={onChange}
showLineNumbers
/>
<div className="">
<button
className="bg-white text-gray-800 rounded-md font-semibold px-4 py-2 my-4"
onClick={onSubmit}
>
<i className="fas fa-play"></i> Run SQL
</button>
</div>
</div>
);
};
export default Editor;
Hook code:
import { useEffect, useState } from "react";
import alasql from "alasql";
import toast from "react-hot-toast";
import TABLE_NAMES from "../utils/tableNames";
const getURL = (name) =>
`https://raw.githubusercontent.com/graphql-compose/graphql-compose-examples/master/examples/northwind/data/csv/${name}.csv`;
const useData = (tableName) => {
const [data, setData] = useState([]);
const [error, setError] = useState(false);
const [runtime, setRuntime] = useState("");
const convertToJson = (data) => {
alasql
.promise("SELECT * FROM CSV(?, {headers: false, separator:','})", [data])
.then((data) => {
setData(data);
toast.success("Query run successfully");
})
.catch((e) => {
toast.error(e.message);
});
};
const fetchData = (tableName) => {
setData([]);
const name = TABLE_NAMES.find((name) => name === tableName);
if (name) {
setError(false);
fetch(getURL(tableName))
.then((res) => res.text())
.then((data) => convertToJson(data));
} else {
setError(true);
toast.error("Please enter a valid query");
}
};
useEffect(() => {
let t0 = performance.now(); //start time
fetchData(tableName);
let t1 = performance.now(); //end time
setRuntime(t1 - t0);
console.log(
"Time taken to execute add function:" + (t1 - t0) + " milliseconds"
);
}, [tableName]);
return { data, runtime, error };
};
export default useData;
If you want to create a context and use it wherever you want, you can create a context, and add the state in this component and pass it to the value prop in the Provider component.
See the sample code.
import React, { createContext, useState } from "react";
export const UserContext = createContext({});
export interface User {
uid: string;
email: string;
}
export const UserProvider = ({ children }: any) => {
const [user, setUser] = useState<User>();
// you can defined more hooks at here
return (
// Pass the data to the value prop for sharing data
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
Then wrap components with the provider function like this
<UserProvider>
<MyComponment1>
</MyComponment1>
<MyComponment2>
</MyComponment2>
<MyComponment3>
</MyComponment3>
</UserProvider>
At This time, Whatever Component in the UserProvider can access the context right now and you can use useContext hook to access the data that you pass in the value props
export const MyComponment1 = () => {
const { user, setUser } = useContext<any>(UserContext);
...
}
I would like to redirect when an axios request is finished and everything has gone okey. But the problem is that I get de following error:
Here is the code:
import React, { useState, Fragment } from "react";
import Sidebar from "../../User/Sidebar";
import NavBar from "../../User/NavBar";
import {
Container,
ContainerRequest,
Text,
Button,
Alert,
AlertContainer
} from "./NoTraineeAccountElements";
import ClassJWT from "../../../../classes/ClassJWT";
import axios from "axios";
import { serverPath } from "../../../../services/serverPath";
import { history } from "../../../../services/history";
const NoTraineeAccount = () => {
const [isOpen, setIsOpen] = useState(false);
const [error, setError] = useState();
const toggle = () => {
setIsOpen(!isOpen);
};
const upgrade = async () => {
const JWT = new ClassJWT();
const axiosReq = axios.create();
await JWT.checkJWT();
axiosReq
.post(`${serverPath}/upgrade-to-trainer`, { token: JWT.getToken()})
.then((res) => {
if (res.data.statusCode === "200") history.push("/trainer");
else setError(res.data.msg);
})
.catch((err) => {
setError(err);
});
};
return (
<Fragment>
<Sidebar isOpen={isOpen} toggle={toggle} />
<NavBar toggle={toggle} />
<Container>
{error ? (
<AlertContainer>
<Alert className="alert alert-danger">{error}</Alert>
</AlertContainer>
) : (
<Fragment></Fragment>
)}
<ContainerRequest>
<Text>{`Your account isn't a trainer account. Would you like to upgrade to a trainer account?`}</Text>
<Button onClick={() => upgrade()}>
<div className="btn btn-primary">Upgrade to Trainer Account</div>
</Button>
</ContainerRequest>
</Container>
</Fragment>
);
};
export default NoTraineeAccount;
Maybe could be useful the services/history code, here it is:
import { createBrowserHistory } from "history";
export const history = createBrowserHistory();
I used history in other components and everything was okey, but I don't know why now is not working. If someone know how to fix it, please let me know.
This isn't about history.
The only code in your snippet that could yield that error is
<Alert className="alert alert-danger">{error}</Alert>
if error is an object with the keys named in the message in your screenshot. Using JSON.stringify() will sidestep this problem, so you can dig deeper into why something strange is being setErrored.
<Alert className="alert alert-danger">{JSON.stringify(error)}</Alert>
(ps. are you sure you should be looking at res.data.statusCode? Maybe res.statusCode? Are you sure it'd be a string "200", not a number 200?)
I am using useEffect to hit an api and display some data from the response.It works well in console but when i try to display the data in a component it throws an error.I am checking for the loading state though.I am showing the data after a i get a response then where does this null coming from
App.js file:
import { useState, useEffect } from 'react';
import Details from './components/Details/Details';
import Header from './components/Header/Header';
import GlobalStyle from './globalStyles';
const API_KEY = 'Private';
// const URL = `https://geo.ipify.org/api/v1?apiKey=${API_KEY}&ipAddress=${ip}`;
function App() {
const [ip, setIp] = useState('8.8.8.8');
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const res = await fetch(
`https://geo.ipify.org/api/v1?apiKey=${API_KEY}&ipAddress=${ip}`
);
const json = await res.json();
setResponse(json);
setIsLoading(false);
} catch (error) {
setError(error);
}
};
fetchData();
// return { response, error, isLoading };
}, [ip]);
return (
<>
<GlobalStyle />
<Header getIp={(q) => setIp(q)} />
<Details isLoading={isLoading} res={response} error={error} />
</>
);
}
export default App;
Header.js file:
import { useState } from 'react';
import { FaArrowRight } from 'react-icons/fa';
import React from 'react';
import { Form, FormInput, Head, HeadLine, Button } from './Header.elements';
// import { useFetch } from '../../useFetch';
const Header = ({ getIp }) => {
const [input, setInput] = useState('');
const onChange = (q) => {
setInput(q);
getIp(q);
};
return (
<>
{/* styled components */}
<Head>
<HeadLine>IP Address Tracker</HeadLine>
<Form
onSubmit={(e) => {
e.preventDefault();
onChange(input);
setInput('');
}}
>
<FormInput
value={input}
onChange={(e) => {
setInput(e.target.value);
}}
placeholder='Search for any IP address or Domain'
/>
<Button type='submit'>
<FaArrowRight />
</Button>
</Form>
</Head>
</>
);
};
export default Header;
Details.js file:
import React from 'react';
import { Box, Location } from './Details.elements';
const Details = ({ res, error, isLoading }) => {
console.log(res);
return isLoading ? (
<div>loading...</div>
) : (
<>
<Box>
<Location>{res.location.city}</Location>
</Box>
</>
);
};
export default Details;
the error it shows:
That happens because on the first render, Details component will receive isLoading=false and res=null, so it will try to render the box so it's throwing the error.
You can initialize isLoading as true.
const [isLoading, setIsLoading] = useState(true);
Or render the Location if res has some value.
<Box>
{res && <Location>{res.location.city}</Location>}
</Box>
According to React documentation :
https://reactjs.org/docs/hooks-reference.html
By default, effects run after every completed render, but you can
choose to fire them only when certain values have changed.
So your component is rendering at least once with isLoading as false before even the API call starts.
You have two choices here:
Set isLoading initial value to true
Add optional chaining res?.location.city
https://codesandbox.io/s/stackoverflow-67755606-uuhqk
The setWishlists hook in this component seems to not run, even though everything before and after it in the promise chain runs. It just doesn't change wishlists. In my test's setup: handleGetWishlists is passed through a jest mock so that it can still be used in the component while allowing jest to spy on it. The implementation is still passed through so that Mock Service Worker an provide the data instead of mocking fetch.
My repo on the relevant branch is here
Relevant section:
//HomePage.js
import React, { useEffect, useState } from 'react';
import { makeStyles } from '#material-ui/core/styles';
import {
Accordion,
AccordionSummary,
AccordionDetails,
Typography,
FormGroup,
FormControlLabel,
Checkbox,
} from '#material-ui/core/';
import ExpandMoreIcon from '#material-ui/icons/ExpandMore';
const useStyles = makeStyles(() => ({
homeContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
heading: {},
accordion: {
width: '50%',
},
}));
const HomePage = ({ handleGetWishlists }) => {
const classes = useStyles();
const [wishlists, setWishlists] = useState([]);
useEffect(() => {
handleGetWishlists()
.then((res) => res.json())
.then((data) => {
console.log('DATA BEFORE SET', data); //--> DATA BEFORE SET [...somedata...]
return data;
})
.then(setWishlists) // --> console.error()
.then(() => console.log('WISHLIST AFTER SET', wishlists)); // --> WISHLIST AFTER SET []
console.log('END OF USE EFFECT');
}, []);
return (
<div className={classes.homeContainer}>
{wishlists.map((wishlist, index) => {
return (
<Accordion key={`Accordian${index}`} className={classes.accordion}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='panel1a-content'
id='panel1a-header'
>
<Typography className={classes.heading}>
{`${wishlist.name} by ${wishlist.author}`}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FormGroup>
{wishlist.items.map((item, index) => {
return (
<FormControlLabel
key={`WishlistItemCheckbox${index}`}
control={<Checkbox />}
label={item.name}
/>
);
})}
</FormGroup>
</AccordionDetails>
</Accordion>
);
})}
</div>
);
};
export default HomePage;
//HomePage.test.js
import React from 'react';
import { mount } from 'enzyme';
import { handleGetWishlists } from '../../client/client';
import HomePage from './HomePage';
import { Accordion } from '#material-ui/core/';
describe('HomePage', () => {
let component;
const mockHandleGetWishlists = jest.fn();
beforeEach(async () => {
mockHandleGetWishlists.mockImplementation(handleGetWishlists);
component = mount(<HomePage handleGetWishlists={mockHandleGetWishlists} />);
});
afterEach(() => {
mockHandleGetWishlists.mockReset();
});
it('should load wishlists', async () => {
expect(mockHandleGetWishlists).toBeCalled();
mockHandleGetWishlists()
.then((res) => res.json())
.then(console.log);
expect(component.exists(Accordion)).toBeTruthy();
});
});
//client.jsconst
client = {
//...
handleGetWishlists: () =>
fetch('http://localhost:3001/wishlist', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}),
//...
};
module.exports = client;
The variable wishlists will contain the updated value only in the next run of useEffect(), not yet in the same run, so console.log('WISHLIST AFTER SET', wishlists) still shows the old value.
Your useEffect is (intentionally) only called once, so you can not console.log the updated wishlist inside the useEffect. You just never have it there.
However, I would expect your wishlists.map() should correctly use the updated wishlists, assuming that the data returned from handleGetWishlists() is correct.
If you want to console.log the updated wishlists, you need to use a 2nd useEffect(), like this:
useEffect(() => {
console.log( 'WISHLIST AFTER SET', wishlists );
}, [ wishlists ]);
When setWishlist is used as a callback in the promise chain, its context changes from your react component state to that of the Promise itself. Calling setWishlist directly from the handler should set wishlist as expected.
handleGetWishlists()
.then((res) => res.json())
.then((data) => {
console.log('DATA BEFORE SET', data); //--> DATA BEFORE SET [...somedata...]
setWishlist(data);
})