React: Dispatch not firing on route change - javascript

I have several routes that use the same controller:
<Route component={Search} path='/accommodation(/:state)(/:region)(/:area)' />
and when the route is changed I call the api function from within the component:
componentWillReceiveProps = (nextProps) => {
if (this.props.params != nextProps.params) {
loadSearch(nextProps.params);
}
}
which is an action as follows:
export function loadSearch (params) {
return (dispatch) => {
return dispatch(
loadDestination(params)
).then(() => {
return dispatch(
loadProperties(params)
);
});
};
}
which loads:
export const DESTINATION_REQUEST = 'DESTINATION_REQUEST';
export const DESTINATION_SUCCESS = 'DESTINATION_SUCCESS';
export const DESTINATION_FAILURE = 'DESTINATION_FAILURE';
export function loadDestination (params) {
const state = params.state ? `/${params.state}` : '';
const region = params.region ? `/${params.region}` : '';
const area = params.area ? `/${params.area}` : '';
return (dispatch) => {
return api('location', {url: `/accommodation${state}${region}${area}`}).then((response) => {
const destination = formatDestinationData(response);
dispatch({
type: DESTINATION_SUCCESS,
destination
});
});
};
}
export const PROPERTIES_REQUEST = 'PROPERTIES_REQUEST';
export const PROPERTIES_SUCCESS = 'PROPERTIES_SUCCESS';
export const PROPERTIES_FAILURE = 'PROPERTIES_FAILURE';
export function loadProperties (params, query, rows = 24) {
return (dispatch, getState) => {
const locationId = getState().destination.id || 99996;
return api('search', {locationId, rows}).then((response) => {
const properties = response.results.map(formatPropertiesData);
dispatch({
type: PROPERTIES_SUCCESS,
properties
});
});
};
}
On initial page load this works and returns data from an api and renders the content. However on changing the route, the loadSearch function is fired but the dispatch (which returns the actual data) doesn't.

Please change your code to this. You missed a dispatch.
Assumption : You are using redux-thunk, and the component has access to dispatch via props (connected). Since you mentioned that you are dispatching on page load, I think this is the case.
componentWillReceiveProps = (nextProps) => {
const {dispatch} = this.props;
if (this.props.params != nextProps.params) {
nextProps.dispatch(loadSearch(nextProps.params));
}
}

Related

React - how can I make the return of JSX wait until my useEffect() ended [duplicate]

I have fetch method in useEffect hook:
export const CardDetails = () => {
const [ card, getCardDetails ] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => getCardDetails(data))
}, [id])
return (
<DetailsRow data={card} />
)
}
But then inside DetailsRow component this data is not defined, which means that I render this component before data is fetched. How to solve it properly?
Just don't render it when the data is undefined:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data));
}, [id]);
if (card === undefined) {
return <>Still loading...</>;
}
return <DetailsRow data={card} />;
};
There are 3 ways to not render component if there aren't any data yet.
{data && <Component data={data} />}
Check if(!data) { return null } before render. This method will prevent All component render until there aren't any data.
Use some <Loading /> component and ternar operator inside JSX. In this case you will be able to render all another parts of component which are not needed data -> {data ? <Component data={data} /> : <Loading>}
If you want to display some default data for user instead of a loading spinner while waiting for server data. Here is a code of a react hook which can fetch data before redering.
import { useEffect, useState } from "react"
var receivedData: any = null
type Listener = (state: boolean, data: any) => void
export type Fetcher = () => Promise<any>
type TopFetch = [
loadingStatus: boolean,
data: any,
]
type AddListener = (cb: Listener) => number
type RemoveListener = (id: number) => void
interface ReturnFromTopFetch {
addListener: AddListener,
removeListener: RemoveListener
}
type StartTopFetch = (fetcher: Fetcher) => ReturnFromTopFetch
export const startTopFetch = function (fetcher: Fetcher) {
let receivedData: any = null
let listener: Listener[] = []
function addListener(cb: Listener): number {
if (receivedData) {
cb(false, receivedData)
return 0
}
else {
listener.push(cb)
console.log("listenre:", listener)
return listener.length - 1
}
}
function removeListener(id: number) {
console.log("before remove listener: ", id)
if (id && id >= 0 && id < listener.length) {
listener.splice(id, 1)
}
}
let res = fetcher()
if (typeof res.then === "undefined") {
receivedData = res
}
else {
fetcher().then(
(data: any) => {
receivedData = data
},
).finally(() => {
listener.forEach((cb) => cb(false, receivedData))
})
}
return { addListener, removeListener }
} as StartTopFetch
export const useTopFetch = (listener: ReturnFromTopFetch): TopFetch => {
const [loadingStatus, setLoadingStatus] = useState(true)
useEffect(() => {
const id = listener.addListener((v: boolean, data: any) => {
setLoadingStatus(v)
receivedData = data
})
console.log("add listener")
return () => listener.removeListener(id)
}, [listener])
return [loadingStatus, receivedData]
}
This is what myself needed and couldn't find some simple library so I took some time to code one. it works great and here is a demo:
import { startTopFetch, useTopFetch } from "./topFetch";
// a fakeFetch
const fakeFetch = async () => {
const p = new Promise<object>((resolve, reject) => {
setTimeout(() => {
resolve({ value: "Data from the server" })
}, 1000)
})
return p
}
//Usage: call startTopFetch before your component function and pass a callback function, callback function type: ()=>Promise<any>
const myTopFetch = startTopFetch(fakeFetch)
export const Demo = () => {
const defaultData = { value: "Default Data" }
//In your component , call useTopFetch and pass the return value from startTopFetch.
const [isloading, dataFromServer] = useTopFetch(myTopFetch)
return <>
{isloading ? (
<div>{defaultData.value}</div>
) : (
<div>{dataFromServer.value}</div>
)}
</>
}
Try this:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
if (!data) {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data))
}
}, [id, data]);
return (
<div>
{data && <DetailsRow data={card} />}
{!data && <p>loading...</p>}
</div>
);
};

redux thunk fetch api action and reducer

So decided to use redux-thunk and I have a problem to write a function in my actions and reducer. Actually function looks like this:
async getData() {
if (this.props.amount === isNaN) {
return;
} else {
try {
await fetch(
`https://api.exchangeratesapi.io/latest?base=${this.props.base}`,
)
.then(res => res.json())
.then(data => {
const date = data.date;
const result = (data.rates[this.props.convertTo] * this.props.amount).toFixed(4);
this.setState({
result,
date,
});
}, 3000);
} catch (e) {
console.log('error', e);
}
}
}
Also I already have action types
export const FETCH_DATA_BEGIN = 'FETCH_DATA_BEGIN';
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';
export const FETCH_DATA_FAIL = 'FETCH_DATA_FAIL';
and actions like this
export const fetchDataBegin = () => {
return {
type: actionTypes.FETCH_DATA_BEGIN,
};
};
export const fetchDataSuccess = data => {
return {
type: actionTypes.FETCH_DATA_SUCCESS,
data: data,
};
};
export const fetchDataFail = error => {
return {
type: actionTypes.FETCH_DATA_FAIL,
error: error,
};
};
And then comes the hard part for me where I don't know how to get the same result from function async getData(). I already have just this in my action :
export async function fetchData() {
return async dispatch => {
return await fetch(`https://api.exchangeratesapi.io/latest?base=${this.props.base}`)
.then(res => res.json())
.then(data => {
// <------------------- WHAT NEXT?
}
};
export function fetchData() {
return dispatch => {
fetch(`https://api.exchangeratesapi.io/latest?base=${this.props.base}`)
.then(res => res.json())
.then(data => dispatch(fetchDataSuccess(data)), e => dispatch(fetchDataFail(e)))
}
};
Now this code:
const date = data.date;
const result = (data.rates[this.props.convertTo] * this.props.amount).toFixed(4);
this.setState({
result,
date,
});
goes into your reducer
if(action.type === FETCH_DATA_SUCCESS) {
const date = action.data.date;
const rates = action.data.rates;
return { ...state, rates, date };
}
Now you can use the redux state in your component and make the rest of the calculations there (ones that need this.props).
To dispatch the fetchData action now, you do this.props.dispatch(fetchData()) in your react-redux connected component.
EDIT
Here's how you use the state in the component.
I'm assuming you have created the redux store. something like:
const store = createStore(rootReducer,applyMiddleware(thunk));
Now, you can use the react-redux library's connect function to connect the redux state to your component.
function mapStateToProps(state, ownProps) {
return {
date: state.date,
result: (state.rates[ownProps.convertTo] * ownProps.amount).toFixed(4);
}
}
function mapDispatchToProps(dispatch) {
return {
fetchData: () => dispatch(fetchData())
}
}
export default connect(mapStateToProps,mapDispatchToProps)(YourComponent)
You can use this Higher Order Component in your DOM now and pass the appropriate props to it:
import ConnectedComponent from "./pathTo/ConnectedComponent";
...
return <View><ConnectedComponent convertTo={...} amount={...} /></View>;
And, also inside YourComponent you can now read this.props.date and this.props.result and use them wherever you need to.
You might want to look at selectors in the future to memoize the state and reduce the performance cost of redux.

Store not updated in redux?

In the "Favorite List" reducer
I have two helper function "Add/Remove" item from the array
the Add work well but Remove it does not update the store in the actual time, because I have a checker in my UI that checks if this song_id in the array or not and bassed on it I update the heart icon BUT it does not work well when I dispatch the remove Action, In Other Words "Not Re-render the component"!.
Action File
import {ADD_TO_FAVORITE, REMOVE_FROM_FAVORITE} from './types';
export const addToFavoriteFunction = track_id => {
return {
type: ADD_TO_FAVORITE,
payload: track_id,
};
};
export const removeFromFavoriteFunction = track_id => {
return {
type: REMOVE_FROM_FAVORITE,
payload: track_id,
};
};
Reducer
import {ADD_TO_FAVORITE, REMOVE_FROM_FAVORITE} from '../actions/types';
let initialState = [];
const addSongFav = (songs, songId, flag) => {
if (songs.some(song => song.track_id === songId)) {
return songs;
} else {
let isFav = {track_id: songId, isFavorite: flag};
return [...songs, isFav];
}
};
const removeSongFav = (songs, songId) => {
const newState = songs.filter(song => song.track_id !== songId);
return newState;
};
const isFavoriteReducer = (state = initialState, action) => {
const {payload, type} = action;
switch (type) {
case ADD_TO_FAVORITE: {
return addSongFav(state, payload, true);
}
case REMOVE_FROM_FAVORITE:
return removeSongFav(state, payload);
default:
return state;
}
};
export default isFavoriteReducer;
"Music Player Component"
....
checkFavorite = () => {
let {currentTrackIndex, tunes} = this.state;
console.log(tunes[currentTrackIndex].id);
let id = tunes[currentTrackIndex].id;
let songs = this.props.favorite;
let isFavorite = songs.some(song => song.track_id === id);
this.setState({isFavorite});
};
componentDidMount() {
this.checkFavorite();
}
addToFavorite = async () => {
const {tunes, token, currentTrackIndex} = this.state;
this.setState({isFavorite: true});
let id = tunes[currentTrackIndex].id;
try {
this.props.addToFavoriteAction(id);
let AuthStr = `Bearer ${token}`;
const headers = {
'Content-Type': 'application/json',
Authorization: AuthStr,
};
// here i send a hit the endoint
} catch (err) {
this.setState({isFavorite: false});
console.log(err);
}
};
deleteFromFavorite = async () => {
const {tunes, token, isFavorite, currentTrackIndex} = this.state;
let id = tunes[currentTrackIndex].id;
this.props.removerFromFavoriteAction(id);
try {
let AuthStr = `Bearer ${token}`;
const headers = {
'Content-Type': 'application/json',
Authorization: AuthStr,
};
// here i send a hit the endoint
} catch (err) {
console.log(err);
}
};
<Button onPress={() => this.state.isFavorite
? this.deleteFromFavorite()
: this.addToFavorite()} >
<Icon name={this.state.isFavorite ? 'favorite' : 'favorite-border'} />
</Button>
....
const mapDispatchToProps = dispatch => {
return {
incrementCount: count => {
dispatch(incrementCount(count));
},
addToFavoriteAction: track_id => {
dispatch(addToFavoriteFunction(track_id));
},
removerFromFavoriteAction: track_id => {
dispatch(removeFromFavoriteFunction(track_id));
},
};
};
mapStateToProps = state => {
return {
favorite: state.favorite,
};
};
export default connect(mapStateToProps, mapDispatchToProps)(MusicPlayer);
Thanks for the live demo, it helped a lot to see the whole picture. The issue is that your view is not actually using the values in your Redux store at all. The reducer is fine and everything is working behind the scenes, but take a look...
const mapStateToProps = state => {
return {
favorite: state,
};
};
This is your mapStateToProps method, and favorite contains an array of the favorite tracks that is successfully being updated whenever you dispatch an action. The reason why your view is not updated accordingly is that you're not using this array anywhere.
<Icon
style={{color:"#00f"}}
type="MaterialIcons"
name={this.state.isFavorite ? 'favorite' : 'favorite-border'}
/>
In this piece of code, what you're checking is the value of a isFavorite property inside of your component's inner state. The reason why it works when you add a favorite is because you're calling setState at the beginning of addToFavorite. On the contrary, deleteFromFavorite is missing that setState call, which is the reason your icon is not changing.
If you want to use what you have in the Redux store to determine which icon to show, you should change your code so it uses this.props.favorite, which is the property that actually references the store and changes according to your actions.
const isCurrentTrackFavorite = () => {
const { tunes, currentTrackIndex } = this.state;
const currentTrackId = tunes[currentTrackIndex].track_id;
// Check array in the Redux store to see if the track has been added to favorites
return this.props.favorite.findIndex(track => track.track_id === currentTrackId) != -1;
};
render() {
<Icon
style={{color:"#00f"}}
type="MaterialIcons"
name={isCurrentTrackFavorite() ? 'favorite' : 'favorite-border'}
/>
}
By making this change, your component will be really listening to the contents of the store and should update the view whenever the array of favorites changes.

How is my action get dispatched without me calling dispatch?

So i've forgot to dispatch my action and just called the function directly and i've noticed that it actually works and i have no idea why.
Can anybody explain to me why/how it works ?
// actions
export const resetSearchBar = () => ({
type: types.RESET_SEARCHBAR,
});
// Component
fetchProducts = () => {
const { productName } = this.state;
const { fetchProductsByName, resetSearchBar } = this.props;
if (productName) {
fetchProductsByName(productName);
return;
}
resetSearchBar(); <-- no dispatch ?
}
const mapDispatchToProps = {
fetchProductsByName,
resetSearchBar,
}
export default connect(null, mapDispatchToProps)(SearchBar);

Call action with params

I'm using React + Redux + Rxjs + typesafe-actions + TS and I want to call action with params. My code for now:
Actions:
import { createAsyncAction } from 'typesafe-actions';
import {ICats} from '/api/cats';
export const FETCH_CATS_REQUEST = 'cats/FETCH_CATS_REQUEST';
export const FETCH_CATS_SUCCESS = 'cats/FETCH_CATS_SUCCESS';
export const FETCH_CATS_ERROR = 'cats/FETCH_CATS_ERROR';
export const fetchCats = createAsyncAction(
FETCH_CATS_REQUEST,
FETCH_CATS_SUCCESS,
FETCH_CATS_ERROR
) <void, ICats, Error> ();
Call dispatch:
store.dispatch(fetchCats.request());
My epics:
const fetchCatsFlow: Epic<Types.RootAction, Types.RootAction, Types.RootState> = (action$) =>
action$.pipe(
filter(isActionOf(fetchCats.request)),
switchMap(() =>
fromPromise(Cats.getDataFromAPI()).pipe(
map(fetchCats.success),
catchError(pipe(fetchCats.failure, of))
)
)
);
API:
export const Cats = {
getDataFromAPI: () => $http.get('/cats').then(res => {
return res.data as any;
}),
};
And it's working - making a call to API but without params. I tried many times and still I don't know how to pass a params when dispatch is called.
I found answer:
export const fetchCats = createAsyncAction(
FETCH_CATS_REQUEST,
FETCH_CATS_SUCCESS,
FETCH_CATS_ERROR
) <void, ICats, Error> ();
changed to:
type ICatsRequest = {
catType: string;
};
export const fetchCats = createAsyncAction(
FETCH_CATS_REQUEST,
FETCH_CATS_SUCCESS,
FETCH_CATS_ERROR
) <ICatsRequest, ICats, Error> ();
and then it allows me to pass specified type to dispatch:
store.dispatch(fetchCats.request({catType: 'XXX'}));
also I needed to modify this:
export const Cats = {
getDataFromAPI: (params) => $http.get('/cats', {
params: {
type: params.payload.catType
}
}).then(res => {
return res.data as any;
}),
};
and
switchMap((params) =>
fromPromise(Cats.getDataFromAPI(params)).pipe(
map(fetchCats.success),
catchError(pipe(fetchCats.failure, of))
)
)

Categories

Resources