I want to try and use react to fetch data efficiently using useEffect appropriately.
Currently, data fetching is happening constantly, instead of just once as is needed, and changing when there is a input to the date period (calling different data).
The component is like this,
export default function Analytics() {
const {
sentimentData,
expressionsData,
overall,
handleChange,
startDate,
endDate,
sentimentStatistical,
} = useAnalytics();
return (
UseAnalytics is another component specifically for fetching data, basically just a series of fetches.
i.e.,
export default function useAnalytics() {
....
const { data: sentimentData } = useSWR(
`dashboard/sentiment/get-sentiment-timefilter?startTime=${startDate}&endTime=${endDate}`,
fetchSentiment
);
....
return {
sentimentData,
expressionsData,
overall,
handleChange,
setDateRange,
sentimentStatistical,
startDate,
endDate,
};
}
Thanks in advance,
The apirequest is like this,
export async function apiRequest(path, method = "GET", data) {
const accessToken = firebase.auth().currentUser
? await firebase.auth().currentUser.getIdToken()
: undefined;
//this is a workaround due to the backend responses not being built for this util.
if (path == "dashboard/get-settings") {
return fetch(`/api/${path}`, {
method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: data ? JSON.stringify(data) : undefined,
})
.then((response) => response.json())
.then((response) => {
if (response.error === "error") {
throw new CustomError(response.code, response.messages);
} else {
return response;
}
});
}
return fetch(`/api/${path}`, {
method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: data ? JSON.stringify(data) : undefined,
})
.then((response) => response.json())
.then((response) => {
if (response.status === "error") {
// Automatically signout user if accessToken is no longer valid
if (response.code === "auth/invalid-user-token") {
firebase.auth().signOut();
}
throw new CustomError(response.code, response.message);
} else {
return response.data;
}
});
}
I think using useEffect here is the right approach. i.e.,
useEffect(()=>{
// this callback function gets called when there is some change in the
// state variable (present in the dependency array)
},[state variable])
I'm confused about how to update the constants properly, something like this seems like one approach, but not sure about how I can use useEffect to update these variables properly, or if I should be doing this inside of useAnalytics?
i.e.,
const [analytics, setAnalytics] = useState({
sentimentData: {},
expressionsData: {},
overall: {},
handleChange: () => {},
startDate: '',
endDate: '',
sentimentStatistical:{},
});
useEffect(()=>{
// this callback function gets called when there is some change in the
// state variable (present in the dependency array)
},[state variable])
const {
sentimentData,
expressionsData,
overall,
handleChange,
startDate,
endDate,
sentimentStatistical,
} = useAnalytics();
Realised SWR is a hook, need to use SWR documentation :P
You have to store the requested information in states inside your custom hook. Then you could consume this hook wherever you want. This should work.
Define custom hook
const useAnalitycs = () => {
const [analytics, setAnalytics] = useState({
sentimentData: {},
expressionsData: {},
overall: {},
startDate: '',
endDate: '',
sentimentStatistical:{},
});
const handleChange = () => {
/* */
};
useEffect(() => {
const fetchData = async () => {
// const response = await apiCall();
// setAnalytics(...)
};
fetchData();
}, []); // called once
return {
...analytics,
handleChange
};
};
Consume useAnalytics hook
const ComponentConsumerA = () => {
/*
const { state/methods you need } = useAnalytics()
...
*/
};
const ComponentConsumerB = () => {
/*
const { state/methods you need } = useAnalytics()
...
*/
};
Related
app.patch('/api/todos/:id', async (req, res) => {
try {
const data = await pool.query("UPDATE todolist SET task = $1 WHERE id = $2;", [req.body.task, req.params.id])
res.json(req.body)
} catch (error) {
console.error(error.message)
}
})
I am trying to make a fetch PATCH request, but every time I do, instead of grabbing the value from the alert window and storing its value in my database, it returns null, or an empty string. Not sure why it is doing this, because it works perfectly well on Postman. Any advice would be appreciated.
import React from "react";
class UpdateBtn extends React.Component {
render() {
const updateTodo = (e, alert) => {
fetch('api/todos/' + e, {
method: 'PATCH',
header: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ task: alert })
})
.then(res => res.json())
.catch(error => console.error(error.message))
}
const handleUpdate = (e) => {
const alert = window.prompt("Update Task:")
if (alert.length === 0) {
return undefined;
}
updateTodo(e.target.id, alert)
// window.location.reload()
}
return (
<button
className="updateBtn"
id={this.props.id}
value={this.props.value}
onClick={handleUpdate}>Update</button>
)
}
}
export default UpdateBtn;
I have a custom hook called Api which fetches data from my API & handles my auth token and refresh tokens.
On my Main app, there are multiple ways that my state variable "postId" will be changed. And whenever it is changed, I want my API to fetch the new content for that. But I can't call my custom Api within useEffect, which is how I'm detecting changes in postId.
Can someone please suggest a workaround? I spent forever making this API, now I feel like I can't even use it.
Main.tsx:
import React, {useState, useEffect} from 'react';
import Api from './hooks/useApi';
import Modal from 'react-modal'
import Vim from './Vim';
import './Main.css';
import './modal.css';
Modal.setAppElement('#root')
function Main():JSX.Element {
const [postId,updatePostId] = useState<number|null>(null)
const [content, updateContent] = useState<string>('default text');
const [auth, updateAuth] = useState<boolean>(false)
const [authModalIsOpen, setAuthModal] = useState(false)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [authKey, setAuthKey] = useState('')
const [refreshKey, setRefreshKey] = useState('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYxMjMzNjU4MiwianRpIjoiZTA0YjRlMjQ3MTI2NGY5ZWE4MWRiZjdiYmUzYzYwNzkiLCJ1c2VyX2lkIjoxfQ.TFBBqyZH8ZUtOLy3N-iwikXOLi2x_eKmdZuCVafPWgc')
const apiUrl = 'http://127.0.0.1:8000/'
function openAuthModal(){ setAuthModal(true) }
function closeAuthModal(){
if(auth){ setAuthModal(false) }
}
useEffect(()=>{
const props = {
username: 'raven',
password: 'asdfsdfds',
payload: {
path: 'notes/',
method: 'GET',
body: {pid: postId},
},
complete: (res:{})=>{console.log(res)},
fail: ()=>{}
}
Api(props)
},[postId])
function loadPost(pid:number):string|null{
// fetch from API, load post content
console.log('I can access:'+postId)
return null;
}
function backLinks():JSX.Element{
return(
<div className="backlinks">
</div>
)
}
function sendLogin(){
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: username,
password: password
})
}
return fetch(apiUrl+'login', requestOptions)
.then(response=>response.json())
}
return (
<div className='main'>
<Vim key={postId} content={content} />
<Modal
isOpen={authModalIsOpen}
onRequestClose={closeAuthModal}
className='Modal'
overlayClassName='Overlay'
>
<form onSubmit={(e)=>{
e.preventDefault()
console.log(username)
sendLogin().then((data)=>{
if(data.auth){
updateAuth(true)
}
})
}}>
<input name='username' onChange={(e)=>{
setUsername(e.target.value)
}}/>
<input type="password" name='password' onChange={(e)=>{
setPassword(e.target.value)
}}/>
<button type="submit">Login</button>
</form>
</Modal>
</div>
)
}
export default Main
useApi.tsx:
import {useState, useEffect} from 'react'
interface IProps {
username:string,
password:string,
payload:IPayload,
complete: (result:{})=>void,
fail: ()=>void
}
interface IPayload {
path:string,
method:string,
body:{}|null,
}
function Api(props:IProps){
const [accessKey, setAccessKey] = useState('')
const [refreshKey, setRefreshKey] = useState('')
const [refreshKeyIsValid, setRefreshKeyIsValid] = useState<null|boolean>(null)
const apiUrl = 'http://127.0.0.1:8000/api/'
const [accessKeyIsValid, setAccessKeyIsValid] = useState<null|boolean>(null)
const [results, setResults] = useState<null|{}>(null)
function go(payload=props.payload){
const options = {
method: payload.method,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer '+accessKey,
},
... (payload.body !== null) && { body: JSON.stringify(payload.body) }
}
return fetch(apiUrl+payload.path,options)
.then(response=>{
if(response.status===401){
setAccessKeyIsValid(false)
return false
} else {
return response.json()
.then(response=>{
setResults(response)
return true
})
}
})
}
useEffect(()=>{
if(results){
props.complete(results)
}
},[results])
useEffect(()=>{
if(accessKeyIsValid===false){
// We tried to make a request, but our key is invalid.
// We need to use the refresh key
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json', },
body: JSON.stringify( {'refresh': refreshKey} ),
}
fetch(apiUrl+'token/refresh/', options)
.then(response=>{
if(response.status === 401){
setRefreshKeyIsValid(false)
// this needs to trigger a login event
} else {
response.json()
.then(response=>{
setRefreshKeyIsValid(true)
setAccessKey(response.access)
setRefreshKey(response.refresh)
setAccessKeyIsValid(true)
})
}
})
}
},[accessKeyIsValid])
useEffect(()=>{
if(accessKeyIsValid===true){
// Just refreshed with a new access key. Try our request again
go()
}
},[accessKeyIsValid])
useEffect(()=>{
if(refreshKeyIsValid===false){
// even after trying to login, the RK is invalid
// We must straight up log in.
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: props.username,
password: props.password,
})
}
fetch(apiUrl+'api/token/', options)
.then(response=>{
if(response.status === 401){ props.fail() }
else {
response.json()
.then(response=>{
setAccessKey(response.access)
setAccessKeyIsValid(true)
})
}
})
}
},[refreshKeyIsValid])
return( go() )
};
export default Api
You can pass dependencies to your custom hooks to be passed on to any underlying hooks that may depend on them. Since I'm not very familiar with Typescript there may be some necessary type definition tweaks. I've looked over your hook logic and suggest the follow for what I think would be the correct dependencies for when postId changes.
function useApi(props: IProps, deps) { // <-- accept a dependency array arg
const [accessKey, setAccessKey] = useState("");
const [refreshKey, setRefreshKey] = useState("");
const [refreshKeyIsValid, setRefreshKeyIsValid] = useState<null | boolean>(
null
);
const apiUrl = "http://127.0.0.1:8000/api/";
const [accessKeyIsValid, setAccessKeyIsValid] = useState<null | boolean>(
null
);
const [results, setResults] = useState<null | {}>(null);
const go = useCallback(() => { // <-- memoize go callback
const { body, method, path } = props.payload;
const options = {
method,
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + accessKey
},
...(body !== null && { body: JSON.stringify(body) })
};
return fetch(apiUrl + path, options).then((response) => {
if (response.status === 401) {
setAccessKeyIsValid(false);
return false;
} else {
return response.json().then((response) => {
setResults(response);
return true;
});
}
});
}, [accessKey, props.payload, setAccessKeyIsValid, setResults]);
useEffect(() => {
if (results) {
props.complete(results);
}
}, [results, props]);
useEffect(() => {
if (accessKeyIsValid) {
// Just refreshed with a new access key. Try our request again
go();
} else {
// We tried to make a request, but our key is invalid.
// We need to use the refresh key
const options = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh: refreshKey })
};
fetch(apiUrl + "token/refresh/", options).then((response) => {
if (response.status === 401) {
setRefreshKeyIsValid(false);
// this needs to trigger a login event
} else {
response.json().then((response) => {
setRefreshKeyIsValid(true);
setAccessKey(response.access);
setRefreshKey(response.refresh);
setAccessKeyIsValid(true);
});
}
});
}
}, [accessKeyIsValid, ...deps]); // <-- pass your dependencies
useEffect(() => {
if (!refreshKeyIsValid) {
// even after trying to login, the RK is invalid
// We must straight up log in.
const options = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: props.username,
password: props.password
})
};
fetch(apiUrl + "api/token/", options).then((response) => {
if (response.status === 401) {
props.fail();
} else {
response.json().then((response) => {
setAccessKey(response.access);
setAccessKeyIsValid(true);
});
}
});
}
}, [refreshKeyIsValid, ...deps]); // <-- pass your dependencies
return go();
}
Usage
useApi(props, [postId]);
I'm calling an action in componentDidMount as follows
componentDidMount() {
const { allowedEvcCards} = this.props;
allowedEvcCards(id);
}
With these actions i'm doing API calls and receiving some data as the response. I have set the data to a state with my reducer. I want to do some logic in the componentDidMount it self with the data received in the response.
For example in my reducer i'm doing this
case ALLOWED_EVC_SUCCESS:
return {
...state,
allowedEvc: action.data
}
And in componentDidMount i want to use allowedEvc . But it returns undefined as the action call is not complete at the time.
My action
// Get allowed Evc cards
export const ALLOWED_EVC_LOADING = 'ALLOWED_EVC_LOADING';
export const ALLOWED_EVC_SUCCESS = 'ALLOWED_EVC_SUCCESS';
export function allowedEvcCardsLoading() {
return {
type: ALLOWED_EVC_LOADING
}
}
export function allowedEvcCardsSuccess(data) {
return {
type: ALLOWED_EVC_SUCCESS,
data
}
}
export function allowedEvcCards(id) {
return dispatch => {
dispatch(allowedEvcCardsLoading());
axios.get(`${API_URL}/****/****/${id}/*****`, {
headers: {
// 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
})
.then(res => {
console.log("Allowed EVC response ", res.data);
if (res.data.success === true) {
dispatch(allowedEvcCardsSuccess(res.data));
} else {
console.log("error");
// alert("error");
}
})
}
}
Unfortunately, componentDidMount is only called when a component is mounted. Unless, you unmount it you can't use that property. However, you could use componentDidUpdate since it is called as soon as it receives props.
Read more on this lifecycle method.
Edit: maybe you could try returning the axios promise along with the data and use it.
// Component
async componentDidMount() {
const { allowedEvcCards} = this.props;
const data = await allowedEvcCards(id);
// ... do something with data
}
// Action
export function allowedEvcCards(id) {
return dispatch => {
dispatch(allowedEvcCardsLoading());
return axios.get(`${API_URL}/****/****/${id}/*****`, {
headers: {
// 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
})
.then(res => {
console.log("Allowed EVC response ", res.data);
if (res.data.success === true) {
dispatch(allowedEvcCardsSuccess(res.data));
return res.data;
} else {
console.log("error");
// alert("error");
}
})
}
}
I'm posting data from form to my json-server url localhost:3000/recipes and I'm trying to get data in other component without refreshing the page. I'm posting some data to recipes url and when i go back to other hashURL on page I need to refresh my page to get result. Is there any way to get data async from life cycles or something similar ?
componentDidMount() {
recipesService.then(data => {
this.setState({
recipes: data
});
});
}
recipe.service
const url = "http://localhost:3000/recipes";
let recipesService = fetch(url).then(resp => resp.json());
let sendRecipe = obj => {
fetch(url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(obj)
})
.then(resp => resp.json())
.then(data => console.log(data))
.catch(err => console.log(err));
};
module.exports = {
recipesService,
sendRecipe
};
Probably you want to use something like Redux. :)
Or you can create your cache for this component:
// cache.js
let value;
export default {
set(v) { value = v; },
restore() { return value; },
};
// Component.js
import cache from './cache';
...
async componentDidMount() {
let recipes = cache.restore();
if (!recipes) {
recipes = await recipesService;
cache.set(recipes);
}
this.setState({ recipes });
}
I am using React with react-redux, redux and redux-actions.
I have one action that takes the current token stored in localStorage and ensures that it is not expired, like so:
export const verifyLogin = () => {
return verifyLoginAC({
url: "/verify/",
method: "POST",
data: {
token: `${
localStorage.getItem("token")
? localStorage.getItem("token")
: "not_valid_token"
}`
},
onSuccess: verifiedLogin,
onFailure: failedLogin
});
};
function verifiedLogin(data) {
const user = {
...data.user
}
setUser(user);
return {
type: IS_LOGGED_IN,
payload: true
};
}
function failedLogin(data) {
return {
type: IS_LOGGED_IN,
payload: false
};
}
When it verifies the token it returns a response like so:
{
token: "token_data",
user: {
username: "this",
is_staff: true,
(etc...)
}
}
As you can see in verifiedLogin(), it is calling another function (in this case an action creator) to set the user to the user object returned by my API. the setUser is defined like this:
const setUser = createAction(SET_USER);
which should create an Action like this:
{
type: SET_USER,
payload: {
userdata...
}
}
The reducer is defined like this:
import { handleActions } from "redux-actions";
import { SET_USER } from "../constants/actionTypes";
export default handleActions(
{
[SET_USER]: (state, action) => action.payload
},
{}
);
I know the action creator is correct, as I have verified by console.log(setUser(user)); but all that is in the state is an empty object for users. I am unable to determine why it is not working successfully. I am new to React and Redux so it may be something I misunderstood.
Edit:
This is apiPayloadCreator:
const noOp = () => ({ type: "NO_OP" });
export const apiPayloadCreator = ({
url = "/",
method = "GET",
onSuccess = noOp,
onFailure = noOp,
label = "",
data = null
}) => {
console.log(url, method, onSuccess, onFailure, label, data);
return {
url,
method,
onSuccess,
onFailure,
data,
label
};
};
Even though you are calling setUser, it is not being dispatched by Redux, which is what ultimately executes a reducer and updates the store. Action creators like setUser are not automatically wired up to be dispatched; that is done in the connect HOC. You will need a Redux middleware such as redux-thunk to dispatch async / multiple actions. Your code can then be something like the example below (using redux-thunk):
export const verifyLogin = () => (dispatch) => {
return verifyLoginAC({
url: "/verify/",
method: "POST",
data: {
token: `${
localStorage.getItem("token")
? localStorage.getItem("token")
: "not_valid_token"
}`
},
onSuccess: (result) => verifiedLogin(dispatch, result),
onFailure: (result) => diapatch(failedLogin(result))
});
};
const verifiedLogin = (dispatch, data) => {
const user = {
...data.user
};
dispatch(setUser(user));
dispatch({
type: IS_LOGGED_IN,
payload: true
});
};
You're going to need to use something like redux-thunk in order to do async actions. See the documentation on how this is done.