How to save states between route change in React - reactjs

I have a simple application that shows the list of local hotels. Each list item has a <Link/> that redirects to another component, which displays the location on the map for that specific hotel. When switching routes, it seems that the <ProductList/> component gets destroyed and so do all the states in it. So every time when it makes new API calls and re-renders. I tried to save in local storage on each componentWillUnmount and retrieve it in useEffect() so that I can make API calls conditionally, and it works but sometimes doesn't work.
import React, { useState, useEffect} from "react";
import ProductItem from "../Components/ProductItem";
import axios from "axios";
const ProductList = () => {
const [hotelList, setHotelList] = useState([]);
// Get user location by IP
const getCurrentLocation = () => {
return fetch("https://ipinfo.io/json?token=MyToken").then(
(response) => response.json()
);
};
// Get list of hotels in specific location
const getHotelsInLocation = (destInfo) => {
console.log('destInfo is: ', destInfo)
const options = {
method: "GET",
url: "https://booking-com.p.rapidapi.com/v1/hotels/search",
params: {
checkout_date: "2022-10-01",
units: "metric",
dest_id: destInfo.destId,
dest_type: destInfo.destType,
locale: "en-gb",
adults_number: 2,
order_by: "popularity",
filter_by_currency: "USD",
checkin_date: "2022-09-30",
room_number: 1,
},
headers: {
"X-RapidAPI-Host": "booking-com.p.rapidapi.com",
"X-RapidAPI-Key": "MyApiKey",
},
};
axios
.request(options)
.then(function (response) {
console.log(response.data.result);
setHotelList(response.data.result);
})
.catch(function (error) {
console.error(error);
});
};
useEffect(() => {
getCurrentLocation().then((currentLocation) => {
console.log("Current city ", currentLocation.city);
const options = {
method: "GET",
url: "https://booking-com.p.rapidapi.com/v1/hotels/locations",
params: { locale: "en-gb", name: currentLocation.city },
headers: {
"X-RapidAPI-Host": "booking-com.p.rapidapi.com",
"X-RapidAPI-Key":
"MyApiKey",
},
};
axios
.request(options)
.then(function (response) {
console.log(response.data);
let destId = response.data[0].dest_id;
let destType = response.data[0].dest_type;
const destInfo = { destId, destType };
getHotelsInLocation(destInfo);
})
.catch(function (error) {
console.error(error);
});
});
}, []);
return (
<>
{hotelList.map((hotel) => (
<ProductItem key={hotel.hotel_id} hotel={hotel} />
))}
</>
);
};
export default ProductList;
How could I do so when coming back to <ProductList/> component, it doesn't make new API calls but just display the hotelList from the previous call.

In this case, You need to keep the datas in the centralized store. For this, you have 2 options. One is using context api which is default react feature. Another one is using redux which is seperate package.
My opinion is, you can go with context api.
A simple example of context api is given below,
Filecontext.jsx
import { createContext } from 'react'
export const Filecontext = createContext({});
Formcomponent.jsx
import { Filecontext } from '../Contexts/Filecontext';
import { useContext } from 'react'
export default function Formcomponent() {
const { setName, setEmail, setMobileno, showAlert } = useContext(Filecontext)
return (
<>
<div className="form-group">
<label>Name : </label>
<input type="text" onChange={(e) => { setName(e.target.value) }} />
</div>
<div className="form-group">
<label>Email : </label>
<input type="email" onChange={(e) => { setEmail(e.target.value) }} />
</div>
<div className="form-group">
<label>Mobile No : </label>
<input type="number" onChange={(e) => { setMobileno(e.target.value) }} />
</div>
<div className="form-group">
<input type="submit" value="submit" onClick={() => { showAlert() }} />
</div>
</>
)
}
Listcomponent.jsx
import { Filecontext } from '../Contexts/Filecontext';
import { useContext } from 'react'
export default function Listcomponent() {
const { name, email, mobileno } = useContext(Filecontext)
return (
<>
<p>Name :</p>{name}
<p>Email :</p>{email}
<p>Mobile No :</p>{mobileno}
</>
)
}
App.js
import logo from './logo.svg';
import './App.css';
import Formcomponent from './Components/Formcomponent';
import Listcomponent from './Components/Listcomponent';
import { Filecontext } from './Contexts/Filecontext';
import { useState } from 'react'
function App() {
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const [mobileno, setMobileno] = useState("")
return (
<div className="App">
<Filecontext.Provider value={{ name, setName, email, setEmail, mobileno, setMobileno, showAlert }}>
<Formcomponent />
<Listcomponent />
</Filecontext.Provider>
</div>
);
}
export default App;
From the above example, The Formcomponent can be displayed in different route and Listcomponent can be displayed in different route. But even then, the datas can be retained with the help of context.

Although caching with useEffect is possible, My recommendation is to consider using one of the query caching libraries such as:
RTK Query
react-query
Apollo (if you're using GraphQL)
From my experience if you're already using redux toolkit RTK Query will be the fit for you.

The short answer is that
You need the list of hotels in a global state instead of local.
Use context / redux along with a cache policy in API calls and track your state changes.
Skip the API calls based on your application logic and memoize the query result when needed hence in effect memoize the global state.

First set up a global state manager. you can use redux, or context API. I prefer to use zustand.
second config local storage to set visited component data.
third when you navigate to a new component retrieve a list of hotels visited and check if the ID exists or not. if yes no need for an API call and if no call it and save to zustand again.

Related

How can I send two values with Axios in React JS?

I am trying to send a comment to my API. This comment is related to an article. So the three values I need to send are the user name, the content of the comment and the post the comment is related to.
A the moment I just succeeded sending the user name and the content to my API, but it didn't send the post the comment was related to.
Here is what I tried :
import React, { useState } from 'react'
import { useParams } from 'react-router-dom'
import TextField from '#material-ui/core/TextField';
import { Button } from '#material-ui/core'
import CommentsAPI from '../../Services/CommentsAPI'
export default function CommentForm() {
const [comment, setComment] = useState({})
const {id_post} = useParams()
const handleSubmit = async (event) => {
event.preventDefault();
try {
CommentsAPI.create(JSON.parse(`{"data":{"id":${id_post}, "attributes":${JSON.stringify(comment)}}}`))
} catch (error) {
console.log(error)
}
}
const handleChange = (event) => {
const {name, value} = event.currentTarget
setComment({
...comment,
[name]: value
})
}
return (
<form onSubmit={handleSubmit}>
<div>
<TextField
id="pseudo"
label="Pseudo"
type="text"
onChange={handleChange}
name="pseudo"
/>
</div>
<div>
<TextField
id="comment"
label="Comment"
multiline
minRows={2}
onChange={handleChange}
name="content"
/>
</div>
<div>
<Button variant="contained" color="primary" type="submit">
Send
</Button>
</div>
</form>
)
}
import { URL_COMMENTS } from '../config'
import axios from 'axios'
function create(id_post, comment) {
return axios.post(URL_COMMENTS, id_post, comment)
}
const CommentsAPI = {
create
}
export default CommentsAPI
you probably want something like this:
import { URL_COMMENTS } from '../config'
import axios from 'axios'
export default function create(id_post, comment) {
const data = {
//pass your data here
userName : yourUserName,
comment : yourComment,
postRelated : post
}
//sending data through api call
axios.post(URL_COMMENTS, data).then((response)=>{
//here your function returns the data gotten from your api
return response.data;
})
}
It looks like your create(id_post, comment) function requires 2 arguments, while you're passing only one:
CommentsAPI.create(JSON.parse(`{"data":{"id":${id_post}, "attributes":${JSON.stringify(comment)}}}`))
Just try to pass two arguments, like so:
CommentsAPI.create(id_post, JSON.stringify(comment));
Also according to axios docs: https://axios-http.com/docs/api_intro
axios.post uses three arguments:
axios.post(url, data, config), so currently you are passing comment as config to axios. Config is not required argument.
Try this (data object should look like request body provided by your backend):
function create(id_post, comment) {
const data = {
id: id_post,
comment: comment,
};
return axios.post(URL_COMMENTS, data);
}

using dispatch on react

I am new to react and I'm trying to create a register and login page with react-redux and dispatch using the mern stack.
When I am calling the method the function did not run.
I have a file for the login page:
import React from "react";
import {login} from '../../actions/authActions';
export class Login extends React.Component {
constructor(props) {
super(props);
}
checkIfElementIsEmpty = (element) => {
if (!element) {
return false
}
if (element.value.length === 0)
{
return false;
}
return true;
}
handleOnClickLogin = () =>
{
let usernameElement = document.getElementsByName("loginUsername")[0];
let passwordElement = document.getElementsByName("loginPassword")[0];
if (!this.checkIfElementIsEmpty(usernameElement))
{
usernameElement.style.backgroundColor = "#ff000042";
return;
}
if (!this.checkIfElementIsEmpty(passwordElement))
{
passwordElement.style.backgroundColor = "#ff000042";
return;
}
console.log("asd");
login(usernameElement.value, passwordElement.value);
}
setColorToDefault = (e) =>{
e.target.style.backgroundColor = "#f3f3f3";
}
render() {
return <div className="base-container" ref={this.props.containerRef}>
<div className="header">Login</div>
<div className="content">
<div className="image">
{/* <img src={loginImg}/> */}
</div>
<div className="form">
<div className="form-group">
<label htmlFor="username">Username:</label>
<input type="text" name="loginUsername" placeholder="username" onFocus={this.setColorToDefault}/>
</div>
<div className="form-group">
<label htmlFor="password">Password:</label>
<input type="password" name="loginPassword" placeholder="password" onFocus={this.setColorToDefault}/>
</div>
</div>
</div>
<div className="footer">
<button type="button" className="btn" onClick={this.handleOnClickLogin}>
Login
</button>
</div>
</div>
}
}
and a file called "authActions.js" with the function "login" that should send the request to the server and validate the login.
export const login = (email, password) => (dispatch: Function) => {
console.log("bbb");
// Headers
const config = {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': "*"
}
}
// Request body
const body = JSON.stringify({ email, password });
axios
.post('http://${HOST}:${PORT}/api/auth/login', body, config)
.then(res =>
dispatch({
type: LOGIN_SUCCESS,
payload: res.data
})
)
.catch(err => {
dispatch(
returnErrors(err.response.data, err.response.status, 'LOGIN_FAIL')
);
dispatch({
type: LOGIN_FAIL
});
});
}
When handleOnClickLogin is called, I only see the 'aaa' on the console. The 'bbb' is never being printed.
Why this is happening and how I need to use dispatch and react-redux correctly?
Your question needs more detail, but I'll guess and give you an overview of what it should look like.
Redux has a connect method that basically will call a function you pass to it with a dispatch (and getState) parameter. So, given: login = (email, password) => (dispatch: Function). You call login(email, pass); and it returns a function (dispatch, [getState]) => xxx. Redux will handle it by calling it with the store's dispatch.
For this to work, you'll also need to configure redux globally, a store, a provider, but I'm assuming your project already does that. Otherwise you'll need to go to the official docs which are really good https://react-redux.js.org/tutorials/connect
However, if you're new to Redux and don't have all the connect set up, it'll be easier (and recommended way also) to use the Hooks API (It's also recommended in react to use Hooks rather than class components). https://react-redux.js.org/introduction/getting-started#hooks
Back to your code, the important pieces you'll need:
import React from "react";
import { login } from '../../actions/authActions';
import { connect } from 'react-redux';
class MyLoginPage extends React.Component {
handleOnClickLogin = () => {
...
// calling the login bound by redux
this.props.doLogin(usernameElement.value, passwordElement.value);
}
}
const LoginPageHOC = connect(null, {
doLogin: login, // changing names for clarity (we could do login: login)
})(MyLoginPage);
export const LoginPage = LoginPageHOC; // use the HOC instead of the class

How to pass auth token that I can access API?(react question)

I am making a simple app for lending phones with this api but I am unable to access the phone items as the request requires auth token. So I am trying to output this
import React from 'react'
import { MobileContext } from './MobileContext';
import { useContext } from 'react';
import Mobile from './Mobile';
import Navbar from './Navbar';
function MobileList() {
const { mobiles } = useContext(MobileContext);
return (
<div>
<Navbar/>
{mobiles.map((item) => (
<Mobile
vendor={item.vendor}
/>
))}
</div>
)
}
export default MobileList
but after correct login getting this without the phones
this is how my context api is set up but apparently I am unable to access the phones
import React, { useState, useEffect, createContext
} from 'react';
import axios from 'axios';
export const MobileContext = createContext({
mobiles: [],
setMobiles: () => {},
updateMobiles: () => {},
});
export default function MobileProvider(props) {
const [mobiles, setMobiles] = useState([]);
const updateMobiles = (id) => {
axios
.get('https://js-test-api.etnetera.cz/api/v1/phones')
.then((res) => setMobiles(res.data));
};
useEffect(() => {
axios
.get('https://js-test-api.etnetera.cz/api/v1/phones')
.then((res) => setMobiles(res.data));
}, [] );
return (
<MobileContext.Provider value={{ mobiles, setMobiles, updateMobiles}}>
{props.children}
</MobileContext.Provider>
);
}
then there is the login page you have to get through if you want to get to the phones page
import React from 'react'
import axios from 'axios';
import { useState } from 'react';
import { useHistory } from "react-router-dom";
function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
let history = useHistory()
const onSubmit = (e) => {
e.preventDefault();
const getIn = {
"login":email,
"password":password,
};
axios
.post('https://js-test-api.etnetera.cz/api/v1/login', getIn,
{
headers: {
'content-type': 'application/json',
}
}).then((res) => {
console.log(res.data);
history.push("/phones");
})
.catch((error) => console.log(error));
};
return (
<div>
<form >
<label>email</label> <input value={email}
onChange={(e) => setEmail(e.target.value)} type="text"/>
<label>password</label> <input type="text" value={password}
onChange={(e) => setPassword(e.target.value)}/>
<button onClick={onSubmit}>login</button>
</form>
</div>
)
}
export default Login
apreciate any advice of how to pass the auth tokens as I have never done this here is the full code
The idea of tokens is that once a user successfully logs in (the POST request), he receives a token from the server (the login's response).
Once a user has his token (stored preferably in a browser's localStorage, to keep it regardless the browser's refresh), he passes this token along with every request to the server that needs authentication.
I.e., for JWT tokens that header is:
Authorization: Bearer [token]

Why cannot I grab items from api/context?(react question)

I am trying to create a simple react app for lending phones with this api.
I am trying to grab the mobiles with context api like this:
import React, { useState, useEffect, createContext
} from 'react';
import axios from 'axios';
export const MobileContext = createContext({
mobiles: [],
setMobiles: () => {},
updateMobiles: () => {},
});
export default function MobileProvider(props) {
const [mobiles, setMobiles] = useState([]);
const updateMobiles = (id) => {
axios
.get('https://js-test-api.etnetera.cz/api/v1/phones')
.then((res) => setMobiles(res.data));
};
useEffect(() => {
axios
.get('https://js-test-api.etnetera.cz/api/v1/phones')
.then((res) => setMobiles(res.data));
}, [] );
return (
<MobileContext.Provider value={{ mobiles, setMobiles, updateMobiles }}>
{props.children}
</MobileContext.Provider>
);
}
and reuse them at the main page after logging in
import React from 'react'
import { MobileContext } from './MobileContext';
import { useContext } from 'react';
import Mobile from './Mobile';
import Navbar from './Navbar';
function MobileList() {
const { mobiles } = useContext(MobileContext);
return (
<div>
<Navbar/>
{mobiles.map((item) => (
<Mobile
vendor={item.vendor}
/>
))}
</div>
)
}
export default MobileList
and this is the single mobile component
import React from 'react'
function Mobile(props) {
return (
<div>
<p>{props.vendor}</p>
<p> ssssssssssss</p>
</div>
)
}
export default Mobile
after the correct logging in, it should display both the text and the vendor for each mobile but it isnt displaying anything besides the navbar
this would probably mean, that I am not getting the mobiles from the api in the first place, but I am not sure why is that. The auth token could also be the reason why I am not able to access the phones,never used it before.
Anyway, this is the full code and I would apreciate any help
login.js
import React from 'react'
import axios from 'axios';
import { useState } from 'react';
import { useHistory } from "react-router-dom";
function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
let history = useHistory()
const onSubmit = (e) => {
e.preventDefault();
const getIn = {
"login":email,
"password":password,
};
axios
.post('https://js-test-api.etnetera.cz/api/v1/login', getIn,
{
headers: {
'content-type': 'application/json',
}
}).then((res) => {
console.log(res.data);
history.push("/phones");
})
.catch((error) => console.log(error));
};
return (
<div>
<form >
<label>email</label> <input value={email}
onChange={(e) => setEmail(e.target.value)} type="text"/>
<label>password</label> <input type="text" value={password}
onChange={(e) => setPassword(e.target.value)}/>
<button onClick={onSubmit}>login</button>
</form>
</div>
)
}
export default Login
As you said, it's the get api expecting an auth token. You need to first login using the login endpoint and get the token from the login response. Post that you can pass that auth token in each get request in the header.
You can update your context file like so :-
import React, { useState, useEffect, createContext
} from 'react';
import axios from 'axios';
export const MobileContext = createContext({
login:()=>{},
mobiles: [],
setMobiles: () => {},
updateMobiles: () => {},
});
export default function MobileProvider(props) {
const [mobiles, setMobiles] = useState([]);
const [token,setToken] = useState(null);
const login = (username,password) =>{
// do the axios post thing - take ref from docs you shared for request body
// get the token from the response and you can set it in the state
setToken(token);
}
const updateMobiles = (id) => {
//Update this get request with proper header value using token state as well.
axios
.get('https://js-test-api.etnetera.cz/api/v1/phones')
.then((res) => setMobiles(res.data));
};
useEffect(() => {
//Update this get request with proper header value using token state as well.
axios
.get('https://js-test-api.etnetera.cz/api/v1/phones')
.then((res) => setMobiles(res.data));
}, [] );
return (
<MobileContext.Provider value={{ login,mobiles, setMobiles, updateMobiles }}>
{props.children}
</MobileContext.Provider>
);
}
Note - How you wan't to use that login function is upto you but generally its through form submission. In your case I think it's an auto login inside an useEffect, so don't hardcode username and password in the UI. You can use environment variables for the same.

Persisting data without redux-persist or localStorage

So, once again, I've been facing this issue of persisting the state tree. In login, for the user to persist, I dispatched an action from my main App.js and got the current logged in user like this:
App.js
componentDidMount() {
const authToken = localStorage.getItem("authToken")
if (authToken) {
this.props.dispatch({ type: "TOKEN_VERIFICATION_STARTS" })
this.props.dispatch(getCurrentUser(authToken))
}
}
Now, I have a form and when it is submitted I'm redirecting the user to the feed where I will show the post title, description in a card form. But as usual, the postData is disappearing after refresh.
It means do I have to make another route, similar to the /me route that I made for getting the current logged in user? And dispatch an action again from the componentDidMount() in App.js?
NewPostForm.js
import React, { Component } from "react"
import { connect } from "react-redux"
import { addpost } from "../actions/userActions"
class NewpostForm extends Component {
constructor(props) {
super(props)
this.state = {
postTitle: "",
postDescription: "",
maxLength: 140
}
}
handleChange = (event) => {
const { name, value } = event.target
this.setState({
[name]: value
})
}
handleSubmit = () => {
const postData = this.state
this.props.dispatch(addpost(postData, () => {
this.props.history.push("/feed")
})
)
}
render() {
const charactersRemaining = (this.state.maxLength - this.state.postDescription.length)
return (
<div>
<input
onChange={this.handleChange}
name="postTitle"
value={this.state.postTitle}
className="input"
placeholder="Title"
maxLength="100"
/>
<textarea
onChange={this.handleChange}
name="postDescription"
value={this.state.postDescription}
className="textarea"
maxLength="140">
</textarea>
<button onClick={this.handleSubmit}>Submit</button>
<div>
Characters remaining: {charactersRemaining}
</div>
</div>
)
}
}
const mapStateToProps = (store) => {
return store
}
export default connect(mapStateToProps)(NewpostForm)
addPost action
export const addpost = (postData, redirect) => {
console.log("inside addpost action")
return async dispatch => {
dispatch({
type: "ADD_post_STARTS"
})
try {
const res = await axios.post("http://localhost:3000/api/v1/posts/new", postData, {
headers: {
"Content-Type": "application/json",
"Authorization": `${localStorage.authToken}`
}
})
dispatch({
type: "ADD_post_SUCCESS",
data: { post: res.data.post },
})
redirect()
} catch (err) {
dispatch({
type: "ADD_post_ERROR",
data: { error: "Something went wrong" }
})
}
}
}
Feed.js
import React from "react";
import { connect } from "react-redux";
const Feed = (props) => {
// const postTitle = (props.post && props.post.post.post.postTitle)
return (
<div className="card">
<header className="card-header">
<p className="card-header-title">
{/* {postTitle} */}
</p>
</header>
<div className="card-content">
<div className="content">
The text of the post written by the user.
</div>
</div>
<footer className="card-footer">
<a href="#" className="card-footer-item">
Edit
</a>
<a href="#" className="card-footer-item">
Delete
</a>
</footer>
</div>
);
};
const mapStateToProps = state => {
return state;
};
export default connect(mapStateToProps)(Feed);
I know you want without redux-persist but the redux normal behavior force to initialize store again from scratch. If you want to persist your state even refresh your page, I would recommend the following package:
https://github.com/rt2zz/redux-persist
If you are losing your state on a page redirect or traveling to a different route using react-router you will want to use:
https://github.com/reactjs/react-router-redux
If I understand correctly it looks like you are using response of /api/v1/posts/new in your feed page however trying to access local state of NewPostForm.js
this.state = {
postTitle: "",
postDescription: "",
maxLength: 140
}
Instead of using local state to save form data which cannot be shared to another component(unless passed as props which is not the case here) you may need to save data to redux store so that it can be shared across different route
handleChange = (event) => {
const { dispatch } = this.props;
const { name, value } = event.target;
dispatch(setPostData(name, value));
}
You action may look like:-
export const setPostData = (name, value) => ({
type: "SET_POST_DATA",
name,
value,
});
After that you can use this.props.postTitle on feed page
Edit: in order to keep state between page reload (full browser reload), you may need to either fetch all data on mount(higher order components are helpful) or use local storage.

Resources