Do I need componentWillReceiveProps and replaceProps when using ReactJS + Redux? - javascript

i'm learning redux along side with react and did a first app to catch a few infos from Destiny and present for the user. The app has a select box where the user can choose one of the many activities and I save that activity to check with the API on the ActivityComponent, the problem is, I do that (get the activity identifier with redux and save on a store) then later I have to retrieve on the ActivityComponent but somehow I had to implement this:
componentWillReceiveProps(nextProps) {
this.props = {};
this.replaceProps(nextProps, cb => this.getAjax());
}
replaceProps(props, callback){
this.props = Object.assign({}, this.props, props);
this.setState(initialState);
callback();
}
Well here's my repository on github if anyone could help me: https://github.com/persocon/destiny-weekly

So the quick answer is no, it's not necessary. Why ? Well, you're not really using redux yet. If you look at that ajax call your are doing in replace props, getAjax, I inspected that in your codebase, and see you're calling setState in the component after receiving a request there.
With redux, you would rather use an action and reducer. The action would be handled, calling the api, and setting the state in the redux "store" with a reducer after receiving this data.
Ok so a full blown example would be something like the following, just first add in redux-thunk, it will definitely help you out going forward, be sure to go read through the example on the README to get a better idea of the how and why.
function startLoading() {
return {
type: 'LOADING_STARTED',
isLoading: true
}
}
function doneLoading(){
return {
type: 'LOADING_ENDED',
isLoading: false
}
}
function setActivity(result) {
let lastGist = result[0];
let activity = {
identifier: result.display.identifier,
title: (result.display.hasOwnProperty('advisorTypeCategory'))? result.display.advisorTypeCategory : '',
name: (result.hasOwnProperty('details') && result.details.hasOwnProperty('activityName')) ? result.details.activityName : '',
desc: (result.hasOwnProperty('details') && result.details.hasOwnProperty('activityDescription')) ? result.details.activityDescription : '',
backgroundImg: (result.display.hasOwnProperty('image')) ? 'http://bungie.net' + result.display.image : '',
modifiers: (result.hasOwnProperty('extended') && result.extended.hasOwnProperty('skullCategories')) ? result.extended.skullCategories : [],
bosses: (result.hasOwnProperty('bosses')) ? result.bosses : [],
items: (result.hasOwnProperty('items') && result.display.identifier == "xur") ? result.items : [],
bounties: (result.hasOwnProperty('bounties')) ? result.bounties : []
}
return {
type: 'SET_ACTIVITY',
activity: activity
}
}
export function findActivity(activity_id) {
return dispatch => {
dispatch(startLoading())
$.get(activity_id, (result)=>{
dispatch(doneLoading())
if(response.status == 200){
dispatch(setActivity(response.json))
}else {
dispatch(errorHere)
}
})
}
}
So it might look a bit intimidating at first, but after a go or two, it will feel more natural doing things this way, instead of in the component.

There shouldn't be any need for replaceProps, as the props will be updated automatically. componentWillReceiveProps is a chance for you to take a peek at what is to come in this lifecycle.
Note: You should never clobber this.props as that is used internally.
I would recommend comparing this.props to nextProps inside componentWillReceiveProps to see if the selected Activity has changed. If so, then fire the ajax call (which I recommend using a redux action passed into the component).

Yeah, I screwed up the comment haha sorry, on the SelectContainer.jsx now I'm doing that to retrieve the activity json after the select change:
const mapDispatchToProps = (dispatch) => {
return {
onSelectChange: (activity) =>{
dispatch(changeApiUrl(activity));
dispatch(findActivity(activity));
}
}
}
UPDATE
import { connect } from 'react-redux';
import { changeApiUrl, findActivity } from '../actions/index.jsx';
import ActivityComponent from '../components/ActivityComponent.jsx';
const mapStateToProps = (state) => {
return state.activity;
}
export class ActivityContainer extends ActivityComponent {
componentDidMount() {
const { dispatch, identifier } = this.props;
dispatch(findActivity(identifier));
}
}
export default connect(mapStateToProps)(ActivityContainer);

Generally speaking on life cycle of methods in react with redux. you should use redux methods. unless you have to use in react life cycle methods.

Related

redux way of rendering data not successful in my sandbox

I am trying to show the data in redux way.
when you click advanced sports search button a drawer opens up in that when you click search attributes it should render history data
so I created this method in the actions fetchHistorySportsDatafromURL
and then called this fetchHistorySportsDatafromURL method in the tabs of the drawer.
but its not displaying the data.
I debugged and found undefined for this const historySports = this.props.historySports;
console.log("historySports--->", historySports);
I am using mapDispatchToProps but still I am not successful.
I am trying to display my data here reading the data from api{historySports}
all my render code is in this file sports-advanced-search.js
providing my code snippet and sandbox below
https://codesandbox.io/s/5x02vjjlqp
actions
export const fetchHistorySportsDatafromURL = data => {
return dispatch => {
if (data != undefined) {
return axios({
method: "get",
url: "https://jsonplaceholder.typicode.com/comments",
data: data,
config: { headers: { "Content-Type": "application/json" } }
})
.then(function(response) {
console.log("fetchSportsHistoryData--->", response);
dispatch(fetchSportsHistoryData(response.data));
})
.catch(function(response) {
dispatch(fetchSportsHistoryData([]));
});
}
};
};
render code
getHistoryData = values => {
this.props.fetchHistorySportsDatafromURL();
};
render() {
const { classes } = this.props;
const { value } = this.state;
const Sports = this.props.Sports;
const historySports = this.props.historySports;
console.log("historySports--->", historySports);
console.log("this.props--->", this.props);
console.log("this.state--->", this.state);
}
const mapDispatchToProps = dispatch => {
return {
fetchHistorySportsDatafromURL: () => {
dispatch(fetchHistorySportsDatafromURL());
}
};
};
export default withStyles(styles)(
connect(
null,
mapDispatchToProps
)(ScrollableTabsButtonAuto)
);
There are several problems here. Without fixing them for you, here's what I think is confusing you...
State and props are two different things. You can use Redux to mapStateToProps for easy use in your component. You may want to do that once you successfully update the Redux state.
Redux state and component-level state are also two different things. The this.state that is defined in the constructor is that component's state. You can can't just add another state object and expect setState to update it. Your state object is not bound to the class.
To update the Redux state, your idea to dispatch(fetchSportsHistoryData(response.data) from the response is on the right track. Now you need for that action to return the data to the state. You need to make sure you have a reducer listening for FETCH_HISTORY_Sports, which responds by updating the Redux state.
Now if you have that part together, you can now use mapStateToProps in your component and then you can access it with this.props.historySports.

In React / Redux, how to call the same fetch twice in componentDidMount, setting 2 state variables with results

The title is wordy, however a short / simple example will go a long ways in explaining my question. I have the following start to a component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchGames } from '../../path-to-action';
class TeamsApp extends Component {
constructor(props) {
super(props);
this.state = {
oldGames: [],
newGames: []
};
}
componentDidMount() {
this.props.dispatch(fetchGames('1617'));
this.setState({ oldGames: this.props.teamGameData });
this.props.dispatch(fetchGames('1718'));
this.setState({ newGames: this.props.teamGameData });
}
...
...
}
function mapStateToProps(reduxState) {
return {
teamGameData: reduxState.GamesReducer.sportsData
};
}
export default connect(mapStateToProps)(TeamsApp);
I would like the action / reducer that corresponds with fetchGames() and gamesReducer to be called twice when the component mounts. This action / reducer grabs some sports data, and I am trying to grab data for two separate seasons (the '1617' season and the '1718' season). The fetchGames() is built correctly to handle the season parameter.
With the current setup, the states aren't being set, and my linter is throwing an error Do not use setState in componentDidMount.
Can I pass a callback to this.props.dispatch that takes the results of the fetchGames() (the teamGameData prop), and sets the oldGames / newGames states equal to this object?
Any help with this is appreciated!
Edit: if i simply remove the this.setState()'s, then my teamGameData prop simply gets overridden with the second this.props.dispatch() call...
Edit 2: I'm not 100% sure at all if having the 2 state variables (oldGames, newGames) is the best approach. I just need to call this.props.dispatch(fetchGames('seasonid')) twice when the component loads, and have the results as two separate objects that the rest of the component can use.
Edit 3: I have the following part of my action:
export const fetchSportsDataSuccess = (sportsData, season) => ({
type: FETCH_NBA_TEAM_GAME_SUCCESS,
payload: { sportsData, season }
});
and the following case in my reducer:
case FETCH_NBA_TEAM_GAME_SUCCESS:
console.log('payload', action.payload);
return {
...state,
loading: false,
sportsData: action.payload.sportsData
};
and the console.log() looks like this now:
payload
{ sportsData: Array(2624), season: "1718" }
but i am not sure how to use the season ID to create a key in the return with this season's data....
Edit 4: found solution to edit 3 - Use a variable as an object key in reducer - thanks all for help on this, should be able to take it from here!
Copying data from the redux store to one's component state is an anti-pattern
Instead, you should modify your redux store, for example using an object to store data, so you'll be able to store datas for multiples seasons :
sportsData: {
'1617': { ... },
'1718': { ... },
}
This way you'll be able to fetch both seasons in the same time :
componentDidMount() {
const seasons = ['1718', '1617'];
const promises = seasons.map(fetchGames);
Promise.all(promises).catch(…);
}
And connect them both :
// you can use props here too
const mapStateToProps = (reduxState, props) => ({
// hardcoded like you did
oldGames: reduxState.GamesReducer.sportsData['1617'],
// or using some props value, why not
newGames: reduxState.GamesReducer.sportsData[props.newSeason],
};
Or connect the store as usual and go for the keys:
const mapStateToProps = (reduxState, props) => ({
games: reduxState.GamesReducer.sportsData,
};
…
render() {
const oldGame = this.props.games[1718];
const newGame = this.props.games[1718];
…
}
Redux is you single source of truth, always find a way to put everything you need in Redux instead of copying data in components

React fetch data getting lost

I have a component in which I fetch data based on an item ID that was clicked earlier. The fetch is successful and console.log shows the correct data, but the data gets lost with this.setState. I have componentDidUpdate and componentDidMount in the same component, not sure if this is okay or maybe these two are messing eachother up?
Here is the code:
const teamAPI = 'http://localhost:8080/api/teams/'
const playerAPI = 'http://localhost:8080/api/playersByTeam/'
const matchAPI = 'http://localhost:8080/api/match/'
class View extends Component {
constructor() {
super();
this.state = {
data: [],
playersData: [],
update: [],
team1: [],
team2: [],
matchData: [],
testTeam: [],
};
}
componentDidUpdate(prevProps) {
if (prevProps.matchId !== this.props.matchId) {
fetch(matchAPI + this.props.matchId)
.then((matchResponse) => matchResponse.json())
.then((matchfindresponse) => {
console.log(matchfindresponse);
this.setState({
matchData:matchfindresponse,
testTeam:matchfindresponse.team1.name,
})
})
}
}
componentDidMount() {
fetch(teamAPI)
.then((Response) => Response.json())
.then((findresponse) => {
console.log(findresponse)
this.setState({
data:findresponse,
team1:findresponse[0].name,
team2:findresponse[1].name,
})
})
fetch(playerAPI + 82)
.then(playerResponse => playerResponse.json())
.then(players => {
console.log(players)
this.setState({
playersData:players
})
})
}
The first render also gives this warning:
Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
Please check the code for the View component.
Everything from ComponentDidMount works fine in render but {this.state.matchData} and {this.state.testTeam} from componentDidUpdate are empty.
Could the problem be that ComponentDidMount re-renders the component which causes the data from ComponentDidUpdate to be lost and if so, how could I fix this?
Tried ComponentWillReceiveProps like this but still no luck
componentWillReceiveProps(newProps) {
if (newProps.matchId !== this.props.matchId) {
fetch(matchAPI + newProps.matchId)
.then((matchResponse) => matchResponse.json())
.then((matchfindresponse) => {
console.log(matchfindresponse.team1.name);
console.log(this.props.matchId + ' ' + newProps.matchId);
this.setState({
matchData:matchfindresponse.team1.name,
})
})
}
}
On your componentDidMount you should be using Promise.all. This isn't really your problem, but it does make more sense.
componentDidMount() {
const promises = [
fetch(teamAPI).then(resp => resp.json()),
fetch(playerAPI + 82).then(resp => resp.json())
];
Promise.all(promises).then(([teamData, playerData]) => {
// you can use this.setState once here
});
}
Looks like your componentDidUpdate should be a getDerivedStateFromProps in combination with componentDidUpdate (this is new to react 16.3 so if you are using an older version use the depreciated componentWillReceiveProps). Please see https://github.com/reactjs/rfcs/issues/26. Notice too that now componentDidUpdate receives a third parameter from getDerivedStateFromProps. Please see https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html for more details.
EDIT: Just to add more details.
Your state object should just include other key like matchIdChanged.
Then
// in your state in your constructor add matchId and matchIdChanged then
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.matchId !== prevState.matchId) {
return { matchIdChanged: true, matchId: nextProps.matchId }
}
return null;
}
componentDidUpdate() {
if (this.state.matchIdChanged) {
fetch(matchAPI + this.props.matchId)
.then((matchResponse) => matchResponse.json())
.then((matchfindresponse) => {
console.log(matchfindresponse);
this.setState({
matchData:matchfindresponse,
testTeam:matchfindresponse.team1.name,
matchIdChanged: false // add this
})
})
}
}
instead of using componentDidUpdate() lifecycle hook of react try using getDerivedStateFromProps() lifecycle function if you are using react 16.3, else try using componentWillReceiveProps() for below versions. In my opinion try to avoid the use of componentDidUpdate().
Plus error you are getting is because, setState() function is called, when your component somehow gets unmounted, there can be multiple reasons for this, most prominent being -
check the render function of this component, are you sending null or something, based on certain condition?
check the parent code of component, and see when is the component getting unmounted.
Or you can share these code, so that we might help you with this.
Plus try to debug using ComponentWillUnmount(), put console.log() in it and test it for more clarity.
Hope this helps, thanks

How to make an API call on props change?

I'm creating a hackernews-clone using this API
This is my component structure
-main
|--menubar
|--articles
|--searchbar
Below is the code block which I use to fetch the data from external API.
componentWillReceiveProps({search}){
console.log(search);
}
componentDidMount() {
this.fetchdata('story');
}
fetchdata(type = '', search_tag = ''){
var url = 'https://hn.algolia.com/api/v1/search?tags=';
fetch(`${url}${type}&query=${search_tag}`)
.then(res => res.json())
.then(data => {
this.props.getData(data.hits);
});
}
I'm making the API call in componentDidMount() lifecycle method(as it should be) and getting the data correctly on startup.
But here I need to pass a search value through searchbar component to menubar component to do a custom search. As I'm using only react (not using redux atm) I'm passing it as a prop to the menubar component.
As the mentioned codeblock if I search react and passed it through props, it logs react once (as I'm calling it on componentWillReceiveProps()). But if I run fetchData method inside componentWillReceiveProps with search parameter I receive it goes an infinite loop. And it goes an infinite loop even before I pass the search value as a prop.
So here, how can I call fetchdata() method with updating props ?
I've already read this stackoverflow answers but making an API call in componentWillReceiveProps doesn't work.
So where should I call the fetchdata() in my case ? Is this because of asynchronous ?
Update : codepen for the project
You can do it by
componentWillReceiveProps({search}){
if (search !== this.props.search) {
this.fetchdata(search);
}
}
but I think the right way would be to do it in componentDidUpdate as react docs say
This is also a good place to do network requests as long as you compare the current props to previous props (e.g. a network request may not be necessary if the props have not changed).
componentDidMount() {
this.fetchdata('story');
}
componentDidUpdate(prevProps) {
if (this.props.search !== prevProps.search) {
this.fetchdata(this.props.search);
}
}
Why not just do this by composition and handle the data fetching in the main HoC (higher order component).
For example:
class SearchBar extends React.Component {
handleInput(event) {
const searchValue = event.target.value;
this.props.onChange(searchValue);
}
render() {
return <input type="text" onChange={this.handleInput} />;
}
}
class Main extends React.Component {
constructor() {
this.state = {
hits: []
};
}
componentDidMount() {
this.fetchdata('story');
}
fetchdata(type = '', search_tag = '') {
var url = 'https://hn.algolia.com/api/v1/search?tags=';
fetch(`${url}${type}&query=${search_tag}`)
.then(res => res.json())
.then(data => {
this.setState({ hits: data.hits });
});
}
render() {
return (
<div>
<MenuBar />
<SearchBar onChange={this.fetchdata} />
<Articles data={this.state.hits} />
</div>
);
}
}
Have the fetchdata function in the main component and pass it to the SearchBar component as a onChange function which will be called when the search bar input will change (or a search button get pressed).
What do you think?
Could it be that inside this.props.getData() you change a state value, which is ultimately passed on as a prop? This would then cause the componentWillReceiveProps function to be re-called.
You can probably overcome this issue by checking if the search prop has changed in componentWillReceiveProps:
componentWillReceiveProps ({search}) {
if (search !== this.props.search) {
this.fetchdata(search);
}
}

React stateless functional components and component lifecycle

So I just switched to using stateless functional components in React with Redux and I was curious about component lifecycle. Initially I had this :
// actions.js
export function fetchUser() {
return {
type: 'FETCH_USER_FULFILLED',
payload: {
name: 'username',
career: 'Programmer'
}
}
}
Then in the component I used a componentDidMount to fetch the data like so :
// component.js
...
componentDidMount() {
this.props.fetchUser()
}
...
After switching to stateless functional components I now have a container with :
// statelessComponentContainer.js
...
const mapStateToProps = state => {
return {
user: fetchUser().payload
}
}
...
As you can see, currently I am not fetching any data asynchronously. So my question is will this approach cause problems when I start fetching data asynchronously? And also is there a better approach?
I checked out this blog, where they say If your components need lifecycle methods, use ES6 classes.
Any assistance will be appreciated.
Firstly, don't do what you are trying to to do in mapStateToProps. Redux follows a unidirectional data flow pattern, where by component dispatch action, which update state, which changes component. You should not expect your action to return the data, but rather expect the store to update with new data.
Following this approach, especially once you are fetching the data asynchronously, means you will have to cater for a state where your data has not loaded yet. There are plenty of questions and tutorials out there for that (even in another answer in this question), so I won't worry to put an example in here for you.
Secondly, wanting to fetch data asynchronously when a component mounts is a common use case. Wanting to write nice functional component is a common desire. Luckily, I have a library that allows you to do both: react-redux-lifecycle.
Now you can write:
import { onComponentDidMount } from 'react-redux-lifecycle'
import { fetchUser } from './actions'
const User = ({ user }) => {
return // ...
}
cont mapStateToProps = (state) => ({
user = state.user
})
export default connect(mapStateToProps)(onComponentDidMount(fetchUser)(User))
I have made a few assumptions about your component names and store structure, but I hope it is enough to get the idea across. I'm happy to clarify anything for you.
Disclaimer: I am the author of react-redux-lifecycle library.
Don't render any view if there is no data yet. Here is how you do this.
Approach of solving your problem is to return a promise from this.props.fetchUser(). You need to dispatch your action using react-thunk (See examples and information how to setup. It is easy!).
Your fetchUser action should look like this:
export function fetchUser() {
return (dispatch, getState) => {
return new Promise(resolve => {
resolve(dispatch({
type: 'FETCH_USER_FULFILLED',
payload: {
name: 'username',
career: 'Programmer'
}
}))
});
};
}
Then in your Component add to lifecycle method componentWillMount() following code:
componentDidMount() {
this.props.fetchUser()
.then(() => {
this.setState({ isLoading: false });
})
}
Of course your class constructor should have initial state isLoading set to true.
constructor(props) {
super(props);
// ...
this.state({
isLoading: true
})
}
Finally in your render() method add a condition. If your request is not yet completed and we don't have data, print 'data is still loading...' otherwise show <UserProfile /> Component.
render() {
const { isLoading } = this.state;
return (
<div>{ !isLoading ? <UserProfile /> : 'data is still loading...' }</div>
)
}

Categories