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!
Related
I'am testing the google maps autocomplete API in my react native app. I have a simple text input field which accepts a search term and based on that the app fetches data from the mentioned api. I have used redux to update the status of the locations in the app.
But after adding redux I can't type anything in the text input, It was working fine earlier. I don't see any error, so I'm helpless on how to resolve. I have attached the relevant codes. Helps would be appreciated.
App.js
const store = createStore(reducers, compose(applyMiddleware(thunk)));
const App = () => {
return (
<Provider store={store}>
<StatusBar barStyle="dark-content"/>
<SearchScreen/>
</Provider>
);
};
export default App;
Reducers.js
export default combineReducers({
locations
});
export default (locations = [], action) => {
switch (action.type) {
case actionTypes.FETCH_LOCATIONS:
return action.payload;
default:
return locations;
}
}
SearchScreen.js
const SearchScreen = () => {
const [from, setFrom] = useState("");
const dispatch = useDispatch();
const handleFrom = (str) => {
setFrom(str);
if (from.length >= 5)
dispatch(getLocations(from))
}
return (
<SafeAreaView>
<View style={styles.container}>
<TextInput
style={styles.textInput}
placeholder="from"
value={from}
onChangeText={(str) => handleFrom(str)}/>
<Text>{from}</Text>
</View>
</SafeAreaView>
)
};
export default SearchScreen;
getLocations
export const getLocations = (searchTerm) => async (dispatch) => {
try {
const {response} = await api.fetchLocations(searchTerm);
console.log(`response: ${JSON.stringify(response)}`);
// dispatch({type: actionTypes.FETCH_LOCATIONS, payload: response?.predictions});
} catch (e) {
console.error(e);
}
}
apis
const API = axios.create({
baseURL: 'https://google-maps-autocomplete-plus.p.rapidapi.com/autocomplete'
});
API.interceptors.request.use((req) => {
req.headers = {
'X-RapidAPI-Key': 'API-KEY',
'X-RapidAPI-Host': 'google-maps-autocomplete-plus.p.rapidapi.com'
};
return req;
});
export const fetchLocations = (query) => API.get(`?query=${query}&limit=5`);
async function always returns a Promise. So wait until it has a value before passing it to disptach.
export const getLocations = (searchTerm) => async {
try {
const { data } = await api.fetchLocations(searchTerm);
console.log(`response: ${JSON.stringify(data)}`);
return data;
} catch (e) {
console.error(e);
return [];
}
}
const handleFrom = (str) => {
setFrom(str);
if (str.length >= 5)
getLocations(str).then(data => dispatch(data, {});
}
My question is, when the next js app refreshing/reloading, redux store state not updating. I have the below code inside the component
const Landing = () => {
const freeADS = useSelector((state) => state.ads.freeAds); //this states are working fine without page refresh
useEffect(() => {
dispatch(fetchFreeAds());
}, [])
return(
{freeADS.map((data, i) => {
//some codings.........
})}
)
}
export default Landing;
redux action call
export const fetchFreeAds = () => {
return {
type: ActionTypes.FETCH_FREE_ADS
}
}
after the rootsaga / watch saga get the request, I call the handler like below
export function* handleFreeAds() {
const { response, error } = yield call(fetchFreeAds);
if (response)
{
yield put({type:"SET_FREE_ADS", payload: response.data[0]});
}
else{
}
}
actual api call goes here
export function fetchFreeAds() {
return axios.get('http://xxxxxxxxxx')
.then(response => ({ response }))
.catch(error => ({ error }))
}
I'm getting this error at the moment. pls give some support. thanks
Thanks to #slideshowp2
Problem solved by doing this miner modification. Added freeAds:[ ] backet to the initial state.
export interface State{
freeAds: null
}
export const adReducers = (state = {freeAds:[]}, {type, payload}) => {
switch(type)
case ActionTypes.SET_FREE_ADS:
return {
...state,
freeAds: payload
};
}
I created a very simple React-Redux App and fetching Users and Posts from https://jsonplaceholder.typicode.com/
In my components I am logging Users and Posts data into the console. As far as I see, in the network tab there is one request for Users and 10 requests for Posts. That's correct but in the console, I see 10 Posts requests for each User.
Does it mean ReactJS renders the component 100 times? What is my mistake in this code?
Any help will be greatly appreciated!
My code and codepen link are below
Please check the code in codepen
const { useEffect } = React;
const { connect, Provider } = ReactRedux;
const { createStore, applyMiddleware, combineReducers } = Redux;
const thunk = ReduxThunk.default;
//-- REDUCERS START -- //
const userReducer = (state = [], action) => {
if (action.type === 'fetch_users') return [...action.payload];
return state;
};
const postReducer = (state = [], action) => {
if (action.type === 'fetch_posts') return [...action.payload];
return state;
};
//-- REDUCERS END -- //
//-- ACTIONS START -- //
const fetchUsers = () => async dispatch => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
dispatch({ type: 'fetch_users', payload: response.data });
};
const fetchPosts = userId => async dispatch => {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${userId}/posts`
);
dispatch({ type: 'fetch_posts', payload: response.data });
};
//-- ACTIONS END -- //
const reducer = combineReducers({ users: userReducer, posts: postReducer });
const store = createStore(reducer, applyMiddleware(thunk));
const mapStateToProps = state => {
return { users: state.users, posts: state.posts };
};
const mapDispatchToProps = dispatch => {
return {
getUsers: () => dispatch(fetchUsers()),
getPosts: (id) => dispatch(fetchPosts(id))
};
};
const Users = props => {
console.log('users', props.users);
const { getUsers } = props;
useEffect(() => {
getUsers();
}, [getUsers]);
const renderUsers = () =>
props.users.map(user => {
return (
<div>
<div>{user.name}</div>
<div>
<PostsContainer userId={user.id} />
</div>
</div>
);
});
return <div style={{backgroundColor:'green'}}>{renderUsers()}</div>;
};
const UserContainer = connect(mapStateToProps, mapDispatchToProps)(Users);
const Posts = props => {
console.log('posts' , props.posts);
const { getPosts, userId } = props;
useEffect(() => {
getPosts(userId);
}, [getPosts, userId]);
const renderPosts = () =>
props.posts.map(post => {
return (
<div>
<div>{post.title}</div>
</div>
);
});
return <div style={{backgroundColor:'yellow'}}>{renderPosts()}</div>;
};
const PostsContainer = connect(mapStateToProps, mapDispatchToProps)(Posts);
const App = props => {
return (
<div>
<UserContainer />
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Does it mean ReactJS renders the component 100 times? What is my mistake in this code?
you have a UserContainer, that renders and requests for users;
once fetched users, you have an update state. UserContainer rerenders, and now you have 10 PostContainers;
each PostContainer makes a request to fetch posts, 10 on total;
it results in 10 state updates. UserContainer rerenders 10 times, and each PostContainer rerenders 10 times;
The component doesn't renders 100 times, each PostContainer renders the initial mount then rerenders 10 times. since there are 10 PostContainers and each rerenders 10 times that's why you might think that renders 100 times.
you have some issues. the dependency issue, which was pointed out is the first. getUsers useEffect should have an empty dependency, and userId useEffect, should depend on userId.
to solve the 10 rerenders on UserContainer due to posts, you need to have a different mapStateToProps to each. for UserContainer you will map only users, otherwise you will get 10 updates due to 10 posts requests:
const mapUserStateToProps = state => {
return { users: state.users };
};
with that it solves UserContainer 10 rerenders.
now about PostContainer there is something that needs to be fixed first, your reducer. your reducer replaces last posts with the current call. in the end you will have only the posts that arrived last, not all posts. to fix that you need to spread your state.
const postReducer = (state = [], action) => {
if (action.type === 'fetch_posts') return [...state, ...action.payload];
return state;
};
eventually, if in your project you could have a repeated request to same userId than it would be necessary to have some validation for not adding the same posts again
now it leads us to mapping props to PostContainer. you would need to have a filter on posts based on userId. mapStateToProps takes props as second argument, which enables us to accomplish that:
const mapPostStateToProps = (state, { userId }) => {
return { posts: state.posts.filter(post => post.userId === userId) };
};
this looks the end to solve the issue, but each PostContainer still rerenders 10 times. why does this happens since posts will be the same? that happens because filter will return a new array reference, no matter if its content didn't change.
to solve this issue you can use React.memo. you need to provide the component and a equality function to memo. to compare an array of objects there are some solutions, also few libs that provide some deepEqual function. here I use JSON.stringify to compare, but you are free to use some other one:
const areEqual = (prevProps, nextProps) => {
return JSON.stringify(prevProps.posts) === JSON.stringify(nextProps.posts)
}
you would validate also other props that could change but that's not the case
now apply React.memo to posts:
const PostsContainer = connect(mapPostStateToProps, mapDispatchToProps)(React.memo(Posts, areEqual));
After all that applied, UserContainer will rerender one once, and each PostContainer will rerender only once as well.
here follows link with working solution:
https://codepen.io/rbuzatto/pen/BaLYmNK?editors=0010
final code:
const { useEffect } = React;
const { connect, Provider } = ReactRedux;
const { createStore, applyMiddleware, combineReducers } = Redux;
const thunk = ReduxThunk.default;
//-- REDUCERS START -- //
const userReducer = (state = [], action) => {
if (action.type === 'fetch_users') return [...action.payload];
return state;
};
const postReducer = (state = [], action) => {
if (action.type === 'fetch_posts') return [...state, ...action.payload];
return state;
};
//-- REDUCERS END -- //
//-- ACTIONS START -- //
const fetchUsers = () => async dispatch => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
dispatch({ type: 'fetch_users', payload: response.data });
};
const fetchPosts = userId => async dispatch => {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${userId}/posts`
);
dispatch({ type: 'fetch_posts', payload: response.data });
};
//-- ACTIONS END -- //
const reducer = combineReducers({ users: userReducer, posts: postReducer });
const store = createStore(reducer, applyMiddleware(thunk));
const mapUserStateToProps = state => {
return { users: state.users };
};
const mapPostStateToProps = (state, { userId }) => {
return { posts: state.posts.filter(post => post.userId === userId) };
};
const mapDispatchToProps = dispatch => {
return {
getUsers: () => dispatch(fetchUsers()),
getPosts: (id) => dispatch(fetchPosts(id))
};
};
const Users = props => {
console.log('users', props.users);
const { getUsers } = props;
useEffect(() => {
getUsers();
}, []);
const renderUsers = () =>
props.users.map(user => {
return (
<div key={user.id}>
<div>{user.name}</div>
<div>
<PostsContainer userId={user.id} />
</div>
</div>
);
});
return <div style={{backgroundColor:'green'}}>{renderUsers()}</div>;
};
const UserContainer = connect(mapUserStateToProps, mapDispatchToProps)(Users);
const Posts = props => {
console.log('posts');
const { getPosts, userId } = props;
useEffect(() => {
getPosts(userId);
}, [userId]);
const renderPosts = () =>
props.posts.map(post => {
return (
<div>
<div>{post.title}</div>
</div>
);
});
return <div style={{backgroundColor:'yellow'}}>{renderPosts()}</div>;
};
const areEqual = (prevProps, nextProps) => {
return JSON.stringify(prevProps.posts) === JSON.stringify(nextProps.posts)
}
const PostsContainer = connect(mapPostStateToProps, mapDispatchToProps)(React.memo(Posts, areEqual));
const App = props => {
return (
<div>
<UserContainer />
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
useEffect() renders the component every time something is changed in the dependencies you provided.
Ideally, you should change your components to re-render only when something changes in props. getUser and getPost change on each render. So, it is better to change it to monitor users and posts from state.
In Users:
const { users, getUsers } = props;
useEffect(() => {
getUsers();
}, []); -- Leaving this empty makes it load only on mount.
In Posts:
const { getPosts, userId } = props;
useEffect(() => {
getPosts(userId);
}, [userId]);
I am trying to reproduce something I was doing with Reactjs/ Redux/ redux-thunk:
Show a spinner (during loading time)
Retrieve information from remote server
display information and remove spinner
The approach was to use useReducer and useContext for simulating redux as explained in this tutorial. For the async part, I was relying on redux-thunk, but I don't know if there is any alternative to it for useReducer. Here is my code:
The component itself :
const SearchForm: React.FC<unknown> = () => {
const { dispatch } = React.useContext(context);
// Fetch information when clickin on button
const getAgentsInfo = (event: React.MouseEvent<HTMLElement>) => {
const fetchData:() => Promise<void> = async () => {
fetchAgentsInfoBegin(dispatch); //show the spinner
const users = await fetchAgentsInfo(); // retrieve info
fetchAgentsInfoSuccess(dispatch, users); // show info and remove spinner
};
fetchData();
}
return (
...
)
The data fetcher file :
export const fetchAgentsInfo:any = () => {
const data = await fetch('xxxx');
return await data.json();
};
The Actions files:
export const fetchAgentsInfoBegin = (dispatch:any) => {
return dispatch({ type: 'FETCH_AGENTS_INFO_BEGIN'});
};
export const fetchAgentsInfoSuccess = (dispatch:any, users:any) => {
return dispatch({
type: 'FETCH_AGENTS_INFO_SUCCESS',
payload: users,
});
};
export const fetchAgentsInfoFailure = (dispatch:any) => {
return dispatch({
type: 'FETCH_AGENTS_INFO_FAILURE'
})
};
And my store itself :
import React, { createContext, useReducer } from 'react';
import {
ContextArgs,
ContextState,
ContextAction
} from './types';
// Reducer for updating the store based on the 'action.type'
const Reducer = (state: ContextState, action: ContextAction) => {
switch (action.type) {
case 'FETCH_AGENTS_INFO_BEGIN':
return {
...state,
isLoading:true,
};
case 'FETCH_AGENTS_INFO_SUCCESS':
return {
...state,
isLoading:false,
agentsList: action.payload,
};
case 'FETCH_AGENTS_INFO_FAILURE':
return {
...state,
isLoading:false,
agentsList: [] };
default:
return state;
}
};
const Context = createContext({} as ContextArgs);
// Initial state for the store
const initialState = {
agentsList: [],
selectedAgentId: 0,
isLoading:false,
};
export const ContextProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
const value = { state, dispatch };
Context.displayName = 'Context';
return (
<Context.Provider value={value}>{children}</Context.Provider>
);
};
export default Context;
I tried to partially reuse logic from this article but the spinner is never displayed (data are properly retrieved and displayed).
Your help will be appreciated !
Thanks
I don't see anything in the code you posted that could cause the problem you describe, maybe do console.log in the reducer to see what happends.
I do have a suggestion to change the code and move logic out of the component and into the action by using a sort of thunk action and replacing magic strings with constants:
//action types
const BEGIN = 'BEGIN',
SUCCESS = 'SUCCESS';
//kind of thunk action (cannot have getState)
const getData = () => (dispatch) => {
dispatch({ type: BEGIN });
setTimeout(() => dispatch({ type: SUCCESS }), 2000);
};
const reducer = (state, { type }) => {
if (type === BEGIN) {
return { ...state, loading: true };
}
if (type === SUCCESS) {
return { ...state, loading: false };
}
return state;
};
const DataContext = React.createContext();
const DataProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, {
loading: false,
});
//redux-thunk action would receive getState but
// cannot do that because it'll change thunkDispatch
// when state changes and could cause problems when
// used in effects as a dependency
const thunkDispatch = React.useCallback(
(action) =>
typeof action === 'function'
? action(dispatch)
: action,
[]
);
return (
<DataContext.Provider
value={{ state, dispatch: thunkDispatch }}
>
{children}
</DataContext.Provider>
);
};
const App = () => {
const { state, dispatch } = React.useContext(DataContext);
return (
<div>
<button
onClick={() => dispatch(getData())}
disabled={state.loading}
>
get data
</button>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(
<DataProvider>
<App />
</DataProvider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
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)))