I have a project that calls an API service on componentDidMount(), using the following code
class Posts extends Component {
componentDidMount() {
apiService.get(this.props.filters);
}
}
const mapStateToProps = state => {
return {
filters: state.filters
}
}
My component passes the filters from my redux store on to the API call.
But what if the filters change through another component? I can't seem to find the correct lifecycle method to re-call the api once that happens.
If you want to solve the problem locally you should use the componentDidUpdate method.
componentDidUpdate(oldProps, newProps) {
if (this.notEqual(oldProps.filter, newProps.filter)) {
apiService.get(this.props.filters);
}
}
But I would consider lifting the data to Redux and sending an async action updating the api data from the filter change event handler.
Answer
Got it working using
componentDidUpdate(prevProps) {
if(JSON.stringify(prevProps.filters) !== JSON.stringify(this.props.filters)) {
// make API call
}
}
Using JSON.stringify ensures that 2 empty arrays don't count as different.
Related
I am trying to monitor the change in a redux store attribute by subscribing to the change using redux watch. For one attribute it works fine. For another, I am making an API request in the reducer (can't make async requests in the actions).
I have verified that the redux state does indeed get updated by creating a button to display the new information.
However, my component isn't actually subscribing to the change.
Does anyone know why this is?
import watch from 'redux-watch';
import {store} from '../index.js';
class ListResults extends Component {
constructor() {
super();
this.state = {
profiles: [],
};
}
componentDidMount() {
let w = watch(store.getState, 'searchResults');
store.subscribe(w((newVal, oldVal, objectPath) => {
// not getting called
console.log("Successfully got new searchResults")
}))
}
...
render() {
...
}
...
function mapStateToProps(state) {
return {
searchResults: state.searchResults,
};
}
export default connect(mapStateToProps)(ListResults);
Redux reducers must be pure functions!
... making an API request in the reducer (can't make async requests in the actions).
sounds very much like a incorrect reducer implementation.
In order to make async requests with redux you should look into redux middleware, like redux-thunk, redux-observables or redux-saga
I am trying to understand the best approach to a data fetching challenge I am facing with redux in my react app.
In short, I need to dispatch the same fetch (in this case, fetchPlayerSeasonStats) twice, and save both fetches of data. The first fetch grabs statistics for a single player (via the fetches optional 1st paramter thisPlayerId), and the 2nd fetch omits the paramter and fetches a much larger dataset.
What I've attempted to do below is the following:
(a) fetch playerSeasonStats the first time
(b) in componentDidUpdate(), check that the first fetch was completed (the if condition checking array legnths).
(c) if condition met, use state variable thisPlayersSeasonStats to store the original fetch of data.
(d) then, refetch the larger dataset with another dispatched action.
... other than the warning I'm receiving saying "do not update state in componentDidMount", in general I'm not sure if this approach is correct or if it is an "anti-pattern" / bad React/Redux coding style. I'd like to make sure I'm doing this right so any review of the code below (in particular the componentDidUpdate() function) would be greatly appreciated!
Thanks!
// Import React Components
import React, { Component } from 'react';
import { connect } from 'react-redux';
// Import Fetches
import { fetchPlayerSeasonStats } from '../../../actions/...';
// Create The Component
class MyComponentHere extends Component {
constructor(props) {
super(props);
this.state = {
thisPlayerSeasonStats: []
};
}
componentDidMount() {
let thisPlayerId = this.props.playerInfo._id;
this.props.dispatch(fetchPlayerSeasonStats(thisPlayerId, this.props.appSeason.value));
}
componentDidUpdate(prevProps) {
console.log('prevProps: ', prevProps);
if (this.props.appSeason !== prevProps.appSeason) { this.refetchTeamsStats(); }
if (prevProps.playerSeasonStats.length === 0 && this.props.playerSeasonStats.length === 1) {
this.setState({ thisPlayerSeasonStats: this.props.playerSeasonStats });
this.props.dispatch(fetchPlayerSeasonStats(null, this.props.appSeason.value));
}
}
render() {
// Handle Initial Loading Of Data
if (this.state.thisPlayerSeasonStats.length === 0) { return <LoadingSpinner />; }
// The Return
return (
<div> Return Dont Matter For here </div>
);
}
}
function mapStateToProps(reduxState) {
return {
playerSeasonStats: reduxState.playerSeasonStatsReducer.sportsData,
loading: (reduxState.playerSeasonStatsReducer.loading),
error1: reduxState.playerSeasonStatsReducer.error
};
}
export default connect(mapStateToProps)(MyComponentHere);
The answer is simple.
Lets look at how redux-thunk works.
Redux Thunk middleware allows you to write action creators that return a function instead of an action
I think this is what fetchPlayerSeasonStats essentially do. It returns some async function that fetch players. Redux-thunk helps dispatch it (I think you use Redux-thunk. In case of you use some other async middleware, it should work essentially the same).
So we can write action creator that will return function (as fetchPlayerSeasonStats) but inside will dispatch not actions but another function. So we'll have function dispatching function which will dispatch action :-)
For example
fetchAllPlayerStats (thisPlayerId, appSeasonValue) => dispatch => {
dispatch(fetchPlayerSeasonStats(thisPlayerId, appSeasonValue));
dispatch(fetchPlayerSeasonStats(null, appSeasonValue));
}
Then you can use this.props.dispatch(fetchAllPlayerStats(thisPlayerId, this.props.appSeason.value)) from componentWillMount to fetch all data at once.
Tip. Current implementation of fetchAllPlayerStats will get all data at once. If you add async/await keywords you'll get firstly data for a single player and then larger data set. Modified version will look like
fetchAllPlayerStats (thisPlayerId, appSeasonValue) => async dispatch => {
await dispatch(fetchPlayerSeasonStats(thisPlayerId, appSeasonValue));
await dispatch(fetchPlayerSeasonStats(null, appSeasonValue));
}
Here is simple example to showcase logic
I have a react component which uses the subscribeToMore method on an apollo graphql query to update itself on changes. It does that just fine, but I want to call some outside function when the subscription picks up a change. I want to show some hints in the UI that things have changed, let's call our function 'onUpdate', Where do I call this onUpdate function?
My component has some code that looks like this before the render function:
subscribeToNewPoints = () => {
this.props.studentSessionPointsQuery.subscribeToMore({
document: NEW_POINT_STUDENT_SESSION_SUBSCRIPTION,
variables,
updateQuery: (previous, { subscriptionData }) => {
const newAllPoints = [
subscriptionData.data.Point.node,
...previous.allPoints
]
const result = {
...previous,
allPoints: newAllPoints
}
return result
}
})
}
componentDidMount() {
this.subscribeToNewPoints()
}
Am I supposed to put my onUpdate function inside here, or somewhere else?
Forgive me if this is answered in another question, but all the others I could find appeared to be asking how to update the cache for the query or how to get subscriptions to work in the first place.
React has it's lifecycle event ComponentWillUpdate which called right before your component gets an update.
Or You can use ComponentDidUpdate which called just after your component gets an update.
ComponentWillUpdate() {
/*called just before an update */
onUpdateFunc()
}
ComponentDidUpdate() {
/*called just after an update */
onUpdateFunc()
}
I am using Redux to create my pagination. My problem is, in constructor I ran a method that will parse the url and check if there is anything about pagination. Then it runs an action that will put the data in my store. It all runs smoothly.
Then, I have the componentDidMount method when I run another action - fetching data. And there I use those props I have previously pushed. Unfortunately, the store is at its initial state.
My (simplified) code:
class NewsList extends React.Component {
constructor(props) {
super(props);
this.profile = document.getElementById('articles').getAttribute('data-profile');
this.parseHash();
}
parseHash() {
/* Method for parsing navigation hash
---------------------------------------- */
const hash = window.location.hash.replace('#', '').split('&');
const data = {
page: 1,
offset: 0
};
// hash parsing
this.props.setPagination(data);
}
componentDidMount() {
this.loadNews();
// If I use
// setTimeout(() => { this.loadNews(); })
// it works, but I feel this is hack-ish
}
loadNews() {
this.props.update({
current: {page: this.props.NewsListReducer.current.page, offset: this.props.NewsListReducer.current.offset},
next: {page: this.props.NewsListReducer.next.page, offset: this.props.NewsListReducer.next.offset}
});
}
}
When I console.log(this.props.NewsListReducer), I am getting undefined for both current and next object, but when I use Redux DevTools, the data is there. What can I do?
It seems like there's some asynchronicity somewhere in there. You're probably using react-redux right? I think the asynchronicity comes from the connected component, as it uses setState when the store state has changed. setState schedules an asychronous state update.
Therefore this.props.NewsListReducer isn't up to date in componentDidMount().
I guess this.props.update is an action that will fetch the news, right? Why is it necessary that you provide the paging information from the component to it? E.g. with redux-thunk you can access the store state before dispatching an action. This could be your chance for reading the (up to date) paging information.
E.g.
export function fetchNews() {
return (dispatch, getState) => {
getState().NewsListReducer.current.page; // Up to date
...
fetch(...).then(() =>
dispatch({...}));
}
}
Btw. it could be a good idea to not call your state *Reducer. It's the reducer managing the state, but the reducer isn't part of the state in that manner.
I'm trying to separate a presentational component from a container component. I have a SitesTable and a SitesTableContainer. The container is responsible for triggering redux actions to fetch the appropriate sites based on the current user.
The problem is the current user is fetched asynchronously, after the container component gets rendered initially. This means that the container component doesn't know that it needs to re-execute the code in its componentDidMount function which would update the data to send to the SitesTable. I think I need to re-render the container component when one of its props(user) changes. How do I do this correctly?
class SitesTableContainer extends React.Component {
static get propTypes() {
return {
sites: React.PropTypes.object,
user: React.PropTypes.object,
isManager: React.PropTypes.boolean
}
}
componentDidMount() {
if (this.props.isManager) {
this.props.dispatch(actions.fetchAllSites())
} else {
const currentUserId = this.props.user.get('id')
this.props.dispatch(actions.fetchUsersSites(currentUserId))
}
}
render() {
return <SitesTable sites={this.props.sites}/>
}
}
function mapStateToProps(state) {
const user = userUtils.getCurrentUser(state)
return {
sites: state.get('sites'),
user,
isManager: userUtils.isManager(user)
}
}
export default connect(mapStateToProps)(SitesTableContainer);
You have to add a condition in your componentDidUpdate method.
The example is using fast-deep-equal to compare the objects.
import equal from 'fast-deep-equal'
...
constructor(){
this.updateUser = this.updateUser.bind(this);
}
componentDidMount() {
this.updateUser();
}
componentDidUpdate(prevProps) {
if(!equal(this.props.user, prevProps.user)) // Check if it's a new user, you can also use some unique property, like the ID (this.props.user.id !== prevProps.user.id)
{
this.updateUser();
}
}
updateUser() {
if (this.props.isManager) {
this.props.dispatch(actions.fetchAllSites())
} else {
const currentUserId = this.props.user.get('id')
this.props.dispatch(actions.fetchUsersSites(currentUserId))
}
}
Using Hooks (React 16.8.0+)
import React, { useEffect } from 'react';
const SitesTableContainer = ({
user,
isManager,
dispatch,
sites,
}) => {
useEffect(() => {
if(isManager) {
dispatch(actions.fetchAllSites())
} else {
const currentUserId = user.get('id')
dispatch(actions.fetchUsersSites(currentUserId))
}
}, [user]);
return (
return <SitesTable sites={sites}/>
)
}
If the prop you are comparing is an object or an array, you should use useDeepCompareEffect instead of useEffect.
componentWillReceiveProps() is going to be deprecated in the future due to bugs and inconsistencies. An alternative solution for re-rendering a component on props change is to use componentDidUpdate() and shouldComponentUpdate().
componentDidUpdate() is called whenever the component updates AND if shouldComponentUpdate() returns true (If shouldComponentUpdate() is not defined it returns true by default).
shouldComponentUpdate(nextProps){
return nextProps.changedProp !== this.state.changedProp;
}
componentDidUpdate(props){
// Desired operations: ex setting state
}
This same behavior can be accomplished using only the componentDidUpdate() method by including the conditional statement inside of it.
componentDidUpdate(prevProps){
if(prevProps.changedProp !== this.props.changedProp){
this.setState({
changedProp: this.props.changedProp
});
}
}
If one attempts to set the state without a conditional or without defining shouldComponentUpdate() the component will infinitely re-render
You could use KEY unique key (combination of the data) that changes with props, and that component will be rerendered with updated props.
componentWillReceiveProps(nextProps) { // your code here}
I think that is the event you need. componentWillReceiveProps triggers whenever your component receive something through props. From there you can have your checking then do whatever you want to do.
I would recommend having a look at this answer of mine, and see if it is relevant to what you are doing. If I understand your real problem, it's that your just not using your async action correctly and updating the redux "store", which will automatically update your component with it's new props.
This section of your code:
componentDidMount() {
if (this.props.isManager) {
this.props.dispatch(actions.fetchAllSites())
} else {
const currentUserId = this.props.user.get('id')
this.props.dispatch(actions.fetchUsersSites(currentUserId))
}
}
Should not be triggering in a component, it should be handled after executing your first request.
Have a look at this example from redux-thunk:
function makeASandwichWithSecretSauce(forPerson) {
// Invert control!
// Return a function that accepts `dispatch` so we can dispatch later.
// Thunk middleware knows how to turn thunk async actions into actions.
return function (dispatch) {
return fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
};
}
You don't necessarily have to use redux-thunk, but it will help you reason about scenarios like this and write code to match.
A friendly method to use is the following, once prop updates it will automatically rerender component:
render {
let textWhenComponentUpdate = this.props.text
return (
<View>
<Text>{textWhenComponentUpdate}</Text>
</View>
)
}
You could use the getDerivedStateFromProps() lifecyle method in the component that you want to be re-rendered, to set it's state based on an incoming change to the props passed to the component. Updating the state will cause a re-render. It works like this:
static getDerivedStateFromProps(nextProps, prevState) {
return { myStateProperty: nextProps.myProp};
}
This will set the value for myStateProperty in the component state to the value of myProp, and the component will re-render.
Make sure you understand potential implications of using this approach. In particular, you need to avoid overwriting the state of your component unintentionally because the props were updated in the parent component unexpectedly. You can perform checking logic if required by comparing the existing state (represented by prevState), to any incoming props value(s).
Only use an updated prop to update the state in cases where the value from props is the source of truth for the state value. If that's the case, there may also be a simpler way to achieve what you need. See - You Probably Don't Need Derived State – React Blog.