I currently have a component which builds its payload from the context stored in a redux store.
When completing the API call it then dispatches nextTask which updates the state of the context causing the effect to run again and be stuck in an infinite loop.
I need to be able to access the context when building the payload but only re-fire the fetch event when the url changes. I tried useCallback but may have misunderstood.
Here is the component:
import React, { useCallback, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import Axios from "../../config/AxiosConfig";
import { nextTask } from "../../redux/actions/flowActions";
import { RootState } from "../../redux/reducers/rootReducer";
import { JsonRPC } from "../../types/tasks";
const JsonRpc: React.FC<JsonRPC> = ({
url,
method,
payload,
payload_paths,
...rest
}) => {
const dispatch = useDispatch();
const context = useSelector((state: RootState) => state.context.present);
const getPayload = useCallback(() => {
console.log(context);
}, [context]);
useEffect(() => {
const fetch = async () => {
const payload = getPayload();
const { data } = await Axios({
method,
url,
// ...opts,
});
if (data.result) {
dispatch(nextTask());
}
};
try {
fetch();
} catch (e) {
console.log(e);
}
}, [url, dispatch, method, getPayload]);
return null;
};
export default JsonRpc;
Related
I'm managing a context for the classes of my school so that i don't have to fetch on every page, for this i write this custom component and wrap my entire app in it.I whenever i fetch the classes i store them in the redux store so that i can use it again and again.
And before fetch i check the store if the classes exist there i don't hit the API instead get the classes from there. in my dependency array i put navigate, dispatch and Classes because react was complaining about that. but this cause infinite loop
here is my code
// ** React Imports
import { createContext, useEffect, useState } from 'react';
import { useDispatch, connect } from 'react-redux';
// ** Config
import { toast } from 'react-hot-toast';
// ** custom imports
import { useNavigate } from 'react-router-dom';
import { CLASSES_API_HANDLER } from 'src/redux/actions/classes/classes';
// ** Defaults
const defaultProvider = {
classes: [],
setClasses: () => null,
loading: true,
setLoading: () => null,
};
export const ClassesContext = createContext(defaultProvider);
const ClassesProvider = ({ children, CLASSES }) => {
const dispatch = useDispatch();
const navigate=useNavigate()
const [classes, setClasses] = useState(defaultProvider.classes);
const [loading, setLoading] = useState(defaultProvider.loading);
useEffect(() => {
const getClasses = async () => {
if (CLASSES.length > 0) {
setClasses(CLASSES);
setLoading(false);
return;
}
try {
const Data = await dispatch(CLASSES_API_HANDLER());
setClasses(Data.result);
setLoading(false);
} catch (ex) {
navigate("/error")
console.log(ex);
}
}
getClasses()
}, [CLASSES,navigate,dispatch]);
const values = {
classes:classes,
setClasses:setClasses,
loading:loading,
setLoading:setLoading,
};
return (
<ClassesContext.Provider value={values}>
{children}
</ClassesContext.Provider>
);
};
function mapStateToProps(state) {
return { CLASSES: state.CLASSES };
}
export default connect(mapStateToProps)(ClassesProvider);
I'm trying to fix the infinite loop
I just started with React-Redux and I decided to put such action as axios request in a separate file:
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import axios from "axios";
const GetRequest = () => {
const dispatch = useDispatch();
async function fetchData() {
try {
const commits = await axios.get(`url`
);
const commitsData = await commits.data;
dispatch({ type: "GET_REQUEST", payload: { commitsData } });
} catch (err) {
await console.log(err);
}
}
useEffect(() => {
fetchData();
}, []);
return <div></div>;
};
export default GetRequest;
But how i can dispatch this if my store and react app can't see this file .How to correctly declare the store about the existence of this file.(Because the option with rendering this file in App.js seems terribly wrong to me)
The best way is to use thunk for this logic and dispatch thunk.
import { createAsyncThunk } from '#reduxjs/toolkit';
import axios from 'axios';
// Define thunk somewhere
export const getRequest = createAsyncThunk('getRequest', async (payload: string, thunkAPI) => {
try {
const commits = await axios.get(payload);
const commitsData = await commits.data;
thunkAPI.dispatch({ type: 'GET_REQUEST', payload: { commitsData } });
} catch (err) {
console.log(err);
}
});
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
// Use thunk in your code
export function SomeComponent() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getRequest('https://example.com'));
}, []);
return <div />;
}
My objective is to get the customer details from a fake API through redux. I have migrated to using createSlice() to reduce the boilerplate, but I keep having that error. I have tried and researched as much as I can, but to no avail.
Here is a self contained code: https://stackblitz.com/edit/react-gbhdd9?file=src/App.js
App.js
import React, { useCallback, useState, useEffect } from 'react';
import { customerApi } from './__fakeAPI__/customerApi';
import { getCustomer as getCustomerSlice } from './slices/customers';
export default function App() {
const [customer, setCustomer] = useState(null);
const getCustomer = useCallback(async () => {
try {
//const data = await customerApi.getCustomer(); //works but not what I wanted
const data = getCustomerSlice();
setCustomer(data);
} catch (err) {
console.error(err);
}
}, []);
useEffect(() => {
getCustomer();
}, [getCustomer]);
return <div>{console.log(customer)}</div>;
}
slices/customers.js
import { createSlice } from '#reduxjs/toolkit';
import { customerApi } from '../__fakeAPI__/customerApi';
const initialState = {
customer: {}
};
const slice = createSlice({
name: 'customers',
initialState,
reducers: {
getCustomer(state, action) {
state.customer = action.payload;
}
}
});
export const { reducer } = slice;
export const getCustomer = () => async dispatch => {
const data = await customerApi.getCustomer();
//dispatch seems to not be working
dispatch(slice.actions.getCustomer(data));
};
export default slice;
You needed to dispatch your action to make it work, to access customer you can use useSelector to get access to your state :
https://stackblitz.com/edit/react-v4u5ft?file=src/App.js
You need to make a few changes to App.js to make this work.
The getCustomerSlice() method is an action creator and in your case, it returns a thunk, and to get access to the dispatch method in a thunk, you must first dispatch the action/function returned from that action creator i.e in App.js
export default function App() {
...
const getCustomer = useCallback(async () => {
try {
const data = dispatch(getCustomerSlice());
...
}, []);
...
}
To get access to the dispatch method in App.js, import the useDispatch from react-redux, useDispatch is a function that returns the dispatch method i.e
...
import { useDispatch } from "react-redux"
...
export default function App() {
const dispatch = useDispatch()
...
const getCustomer = useCallback(async () => {
try {
const data = dispatch(getCustomerSlice());
...
}, []);
...
}
When you do this redux-thunk will automatically supply dispatch, getState as parameters to any function that is returned from an action creator. This will cause your implementation to update your store properly.
Since you are expecting a return value from your dispatch method, make this modification to your customers.js file
...
export const getCustomer = () => async dispatch => {
const data = await customerApi.getCustomer();
//dispatch seems to not be working
dispatch(slice.actions.getCustomer(data));
// return the data
return data;
};
...
then in your App.js file await the dispatch function since you defined it as async
...
import { useDispatch } from "react-redux"
...
export default function App() {
const dispatch = useDispatch()
...
const getCustomer = useCallback(async () => {
try {
const data = await dispatch(getCustomerSlice());
...
}, []);
...
}
TLDR
Your App.js should now look like this.
import React, { useCallback, useState, useEffect } from 'react';
import { customerApi } from './__fakeAPI__/customerApi';
import { useSelector, useDispatch} from "react-redux"
import { getCustomer as getCustomerSlice } from './slices/customers';
export default function App() {
const dispatch = useDispatch()
const [customer, setCustomer] = useState(null);
const getCustomer = useCallback(async () => {
try {
//const data = await customerApi.getCustomer(); //works but not what I wanted
const data = await dispatch(getCustomerSlice());
setCustomer(data);
} catch (err) {
console.error(err);
}
}, []);
useEffect(() => {
getCustomer();
}, [getCustomer]);
return <div>{console.log("customers",customer)}</div>;
}
And your customers.js like this
import { createSlice } from '#reduxjs/toolkit';
import { customerApi } from '../__fakeAPI__/customerApi';
const initialState = {
customer: {}
};
const slice = createSlice({
name: 'customers',
initialState,
reducers: {
getCustomer(state, action) {
state.customer = action.payload;
}
}
});
export const { reducer } = slice;
export const getCustomer = () => async dispatch => {
const data = await customerApi.getCustomer();
//dispatch seems to not be working
dispatch(slice.actions.getCustomer(data));
return data;
};
export default slice;
it is important to note that your component has not subscribed to the store yet. so if the customers object in the store is modified by another component then this component won't re-render, to fix this look up the useSelector hook from react-redux
rootReducer
import { combineReducers } from "redux";
import mods from "./mods.js";
export default combineReducers({
mods
})
reducers/mods.js
import { GET_MODS, GET_SPECIFC_MOD } from "../actions/types"
const initialState = {
mods: [],
currMod: []
}
export default function(state = initialState, action) {
switch(action.type) {
case GET_MODS:
return {
...state,
mods: action.payload
}
case GET_SPECIFC_MOD:
return {
...state,
currMod: action.payload
}
default:
return state
}
}
actions/mods.js
import axios from 'axios'
import { GET_MODS, GET_SPECIFC_MOD } from './types'
// get the mods
export const getMods = () => dispatch => {
axios.get('http://localhost:8000/api/mods')
.then(res => {
dispatch({
type: GET_MODS,
payload: res.data
})
}).catch(err => console.log(err))
}
// get single mod
export const getSpecificMod = (title) => dispatch => {
axios.get(`http://localhost:8000/api/mods/${title}`)
.then(res => {
dispatch({
type: GET_SPECIFC_MOD,
payload: res.data
})
}).catch(err => console.log(err))
}
components/download.js
import React from 'react'
import { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { getSpecificMod } from '../actions/mods'
const Download = () => {
useEffect(() => {
const title = window.location.pathname.split('/')[3]
getSpecificMod(title)
})
return (
<></>
)
}
const mapStateToProp = state => ({
currMod: state.mods.currMod
})
export default connect(mapStateToProp, getSpecificMod)(Download)
Response from backend
GET http://localhost:8000/api/mods/function(){return!window.__REDUX_DEVTOOLS_EXTENSION_LOCKED__&&a.dispatch.apply(a,arguments)}
Basically the user clicks on a mod and gets sent to the download section that is handled by 'download.js' the component ('download.js') renders it and reads the window.location to retrieve the title, with redux I want to get the mod so i made a function that takes the title and sends the request 'getMod(title)' but for some reason it is throwing horrible errors that I dont understand, any help is appreciated!
You are not dispatching the action properly in your component. Right now you are actually just calling the getSpecificMod action creator function from your imports. Your Download component doesn't read anything from props so it is ignoring everything that gets created by the connect HOC.
If you want to keep using connect, you can fix it like this:
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import { getSpecificMod } from '../actions/mods'
const Download = ({currMod, getSpecificMod}) => {
const title = window.location.pathname.split('/')[3]
useEffect(() => {
getSpecificMod(title)
}, [title])
return (
<></>
)
}
const mapStateToProps = state => ({
currMod: state.mods.currMod
})
export default connect(mapStateToProps, {getSpecificMod})(Download)
We are now accessing the bound action creator as a prop of the component. mapDispatchToProps is an object which maps the property key to the action.
But it's better to use the useDispatch hook:
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getSpecificMod } from '../actions/mods'
const Download = () => {
const currentMod = useSelector(state => state.mods.currMod);
const dispatch = useDispatch();
const title = window.location.pathname.split('/')[3]
useEffect(() => {
dispatch(getSpecificMod(title));
}, [title, dispatch]);
return (
<></>
)
}
export default Download;
There might be some confusion on terminology here. Your getSpecificMod function is a function which takes dispatch as an argument but it is not a mapDispatchToProps. It is a thunk action creator.
Make sure that you have redux-thunk middleware installed in order to handle this type of action. Or better yet, use redux-toolkit.
Your useEffect hook needs some sort of dependency so that it knows when to run. If you only want it to run once you can use an empty array [] as your dependencies. If you don't specify the dependencies at all then it will re-run on every render.
Does the pathname change? If so, how do you know when? You might want to add an event listener on the window object. Or consider using something like react-router. But that is a separate question.
I have a react-native, redux app, and after upgrading I've started getting some warnings about lifecycle hooks. My code looks like below:
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { selectPosts} from '../redux/selectors/postSelector';
import { getPosts } from '../redux/actions/postActions';
class BasicScreen extends React.Component {
state = {
data: [],
myItems: [],
};
componentWillMount() {
this.getPosts();
}
componentDidMount() {
this.checkforItems();
}
getPosts = async () => {
// Call to a redux action
await this.props.getPosts();
};
checkforItems = async () => {
// myItems in initial state are set from data in
AsyncStorage.getItem('MyItems').then(item => {
if (item) {
this.setState({
myItems: JSON.parse(item),
});
} else {
console.log('No data.');
}
});
};
componentWillReceiveProps(nextProps) {
// Data comes from the redux action.
if (
nextProps.data &&
!this.state.data.length &&
nextProps.data.length !== 0
) {
this.setState({
data: nextProps.data,
});
}
}
render() {
return (
<View>/* A detailed view */</View>
)
}
}
const mapStateToProps = createStructuredSelector({
data: selectPosts,
});
const mapDispatchToProps = dispatch => ({
dispatch,
getPosts: () => dispatch(getPosts()),
});
export default connect(mapStateToProps, mapDispatchToProps)(BasicScreen);
To summarize, I was calling a redux action (this.getPosts()) from componentWillMount(), and then updating the state by props received in componentWillReceiveProps. Now both these are deprecated, and I am getting warnings that these are deprecated.
Apart from this, I am also setting some initial state by pulling some data from storage (this.checkforItems()). This gives me another warning - Cannot update a component from inside the function body of a different component.
To me it looks like the solution lies in converting this into a functional component, however, I'm stuck at how I will call my initial redux action to set the initial state.
UPDATE:
I converted this into a functional component, and the code looks as follows:
import React, { Fragment, useState, useEffect } from 'react';
import { connect } from 'react-redux';
import AsyncStorage from '#react-native-community/async-storage';
import { StyleSheet,
ScrollView,
View,
} from 'react-native';
import {
Text,
Right,
} from 'native-base';
import { createStructuredSelector } from 'reselect';
import {
makeSelectPosts,
} from '../redux/selectors/postSelector';
import { getPosts } from '../redux/actions/postActions';
const BasicScreen = ({ data, getPosts }) => {
const [myData, setData] = useState([]);
const [myItems, setItems] = useState([]);
const checkForItems = () => {
var storageItems = AsyncStorage.getItem("MyItems").then((item) => {
if (item) {
return JSON.parse(item);
}
});
setItems(storageItems);
};
useEffect(() => {
async function getItems() {
await getPosts(); // Redux action to get posts
await checkForItems(); // calling function to get data from storage
setData(data);
}
getItems();
}, [data]);
return (
<View>
<>
<Text>{JSON.stringify(myItems)}</Text>
<Text>{JSON.stringify(myData)}</Text>
</>
</View>
);
}
const mapStateToProps = createStructuredSelector({
data: makeSelectPosts,
});
const mapDispatchToProps = dispatch => ({
dispatch,
getPosts: () => dispatch(getPosts()),
});
export default connect(mapStateToProps, mapDispatchToProps)(BasicScreen);
It works, but the problem is that the first Text - {JSON.stringify(myItems)} - it is rerendering continuously. This data is actually got using checkForItems(). I wanted the useEffect to be called again only when the data updates, but instead something else is happening.
Also, I noticed that setData is not being called correctly. The data becomes available through the prop (data), but not from the state (myData). myData just returns empty array.