ReactJS, componentWillReceiveProps can get data, but render cannot - reactjs

I am working on a ReactJS 15 project using sagas and reselect as middle ware to fetch data. I can successfully get data in componentWillReceiveProps and set the data in the state, however there still not data in the render function first run when I take the data from the state. Anyone knows what's going on here? BTW, I used json-server as mock data server.Below is part of my code:
Component:
constructor(props) {
super(props);
this.state = {
timelineData: [],
};
}
componentDidMount() {
// use react redux to make the api call here
this.props.fetchTimelineData({
id: parse(this.props.location.search.substr(1)).id,
});
}
componentWillReceiveProps(nextProps) {
console.log('nextProps', nextProps);
// Successfully take the data from the nextProps (timelineData is not [])
const { timelineData } = nextProps;
this.setState({
timelineData,
});
}
render() {
// the first render called timelineData is an empty array, which will not able to populate the UI
// which RaiseTimeline is empty
const { timelineData } = this.state;
return (
<RaiseTimelineStyled>
<RaiseDetailsGrid>
<Accordion
title={
<RaiseAccordionHeader image={image} title={'Timeline'} />
}>
<Timeline.wrapper>
<RaiseTimelineStyled.divider>
<RaiseTimelineStyled.container>
<RaiseTimeline timelineEvents={timelineData} />
action.js (works fine):
export const setTimelineData = timelineData => console.log('actions.js', timelineData) || ({
type: ACTIONS.SET_TIMELINE_DATA,
timelineData,
});
Api.js (works fine):
class TimelineAPI {
// payload will be used after backend done
static fetchTimelineData(payload) {
return http.get(`${baseURI}/timeline`).then(result => console.log('api', result.data) || result.data);
}
}
Reducers: (works fine)
function TimelineDataReducer(state = initialState, action) {
switch (action.type) {
case ACTIONS.SET_TIMELINE_DATA:
console.log('reducer', action.timelineData);
return state.set('numbers', action.timelineData);
default:
return state;
}
}
Sagas: (works fine)
export function* fetchTimelineData(action) {
yield put(togglePendingScreen(true));
const { result, error } = yield call(TimelineAPI.fetchTimelineData, action.payload);
if (error) {
yield put(
toggleErrorModal({
isOpen: true,
text: error.code,
source: 'Fetch Timeline Data',
}),
);
} else {
console.log('Sagas', result.timeline);
yield put(ACTIONS.setTimelineData(result.timeline));
}
yield put(togglePendingScreen(false));
}
Selectors(works fine):
import { createSelector } from 'reselect';
const selectTimelineData = state => state.get('TimelinePageData').toJS();
const selectTimeline = () =>
createSelector(selectTimelineData, TimelineDataState => TimelineDataState.numbers);
export { selectTimeline };

To me it seems logical that you have no data on the first run.
The reason is there render() function is called once before the componentDidMount() in the react life cycle. (V15)
Look here for the react 15 life cycle : https://gist.github.com/bvaughn/923dffb2cd9504ee440791fade8db5f9

I got the answer, everything is correct, but another person name but another person name same component and set its state in side the constructor, so it's not able to render in the first time

Related

Redux , state.concat is not a function at rootReducer. And being forced to reRender an element for it to see the state change

So I have this sidebar component where I load my store and my dispatcher
//select
const mapStateToProps = state => {
return { renderedEl: state.renderedEl }
}
function mapDispatchToProps(dispatch) {
return{
renderLayoutElement: element => dispatch(renderLayoutElement(element))
}
}
Then inside the same component this Is how I trigger the dispatcher
renderEl = (el) => {
var elementName = el.target.getAttribute('id');
var renderedElements = this.props.renderedEl; //this is data from the store
for (let key in renderedElements) {
if (key == elementName) {
renderedElements[key] = true
}
}
this.props.renderLayoutElement({renderedElements});
}
Then as I understand it gets sent to the reducer
import {RENDER_LAYOUT_ELEMENT} from "../constants/action-types"
const initialState = {
renderedEl: {
heimdall: false,
skadi: false,
mercator: false
}
}
function rootReducer(state = initialState, action){
if(action.type === RENDER_LAYOUT_ELEMENT){
return Object.assign({},state,{
renderedEl: state.renderedEl.concat(action.payload)
})
}
return state
}
export default rootReducer;
This is its action
import {RENDER_LAYOUT_ELEMENT} from "../constants/action-types"
export function renderLayoutElement(payload) {
return { type: RENDER_LAYOUT_ELEMENT, payload }
};
Now the thing is. Im receiving a
state.renderedEl.concat is not a function at rootreducer / at dispatch
I dont understand why does that happen.
Becuase, actually the store gets updated as I can see, but the console returns that error. And I have to reload the render that uses the props of that store (with an onhover) in order to be able to see the changes. It doesnt happen automatically as it would happen with a state
if(action.type === RENDER_LAYOUT_ELEMENT){
return { ...state, renderedEl: { ...state.renderedEl, ...action.payload } };
}
Duplicate from comments maybe it can be helpful to someone else :)

Lifecycle hooks - Where to set state?

I am trying to add sorting to my movie app, I had a code that was working fine but there was too much code repetition, I would like to take a different approach and keep my code DRY. Anyways, I am confused as on which method should I set the state when I make my AJAX call and update it with a click event.
This is a module to get the data that I need for my app.
export const moviesData = {
popular_movies: [],
top_movies: [],
theaters_movies: []
};
export const queries = {
popular:
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=###&page=",
top_rated:
"https://api.themoviedb.org/3/movie/top_rated?api_key=###&page=",
theaters:
"https://api.themoviedb.org/3/movie/now_playing?api_key=###&page="
};
export const key = "68f7e49d39fd0c0a1dd9bd094d9a8c75";
export function getData(arr, str) {
for (let i = 1; i < 11; i++) {
moviesData[arr].push(str + i);
}
}
The stateful component:
class App extends Component {
state = {
movies = [],
sortMovies: "popular_movies",
query: queries.popular,
sortValue: "Popularity"
}
}
// Here I am making the http request, documentation says
// this is a good place to load data from an end point
async componentDidMount() {
const { sortMovies, query } = this.state;
getData(sortMovies, query);
const data = await Promise.all(
moviesData[sortMovies].map(async movie => await axios.get(movie))
);
const movies = [].concat.apply([], data.map(movie => movie.data.results));
this.setState({ movies });
}
In my app I have a dropdown menu where you can sort movies by popularity, rating, etc. I have a method that when I select one of the options from the dropwdown, I update some of the states properties:
handleSortValue = value => {
let { sortMovies, query } = this.state;
if (value === "Top Rated") {
sortMovies = "top_movies";
query = queries.top_rated;
} else if (value === "Now Playing") {
sortMovies = "theaters_movies";
query = queries.theaters;
} else {
sortMovies = "popular_movies";
query = queries.popular;
}
this.setState({ sortMovies, query, sortValue: value });
};
Now, this method works and it is changing the properties in the state, but my components are not re-rendering. I still see the movies sorted by popularity since that is the original setup in the state (sortMovies), nothing is updating.
I know this is happening because I set the state of movies in the componentDidMount method, but I need data to be Initialized by default, so I don't know where else I should do this if not in this method.
I hope that I made myself clear of what I am trying to do here, if not please ask, I'm stuck here and any help is greatly appreciated. Thanks in advance.
The best lifecycle method for fetching data is componentDidMount(). According to React docs:
Where in the component lifecycle should I make an AJAX call?
You should populate data with AJAX calls in the componentDidMount() lifecycle method. This is so you can use setState() to update your component when the data is retrieved.
Example code from the docs:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("https://api.example.com/items")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.items
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<ul>
{items.map(item => (
<li key={item.name}>
{item.name} {item.price}
</li>
))}
</ul>
);
}
}
}
Bonus: setState() inside componentDidMount() is considered an anti-pattern. Only use this pattern when fetching data/measuring DOM nodes.
Further reading:
HashNode discussion
StackOverflow question

How To Access Props In a functional components

I have some problem with component
When i use props i get same data in all of mates, but in GET_ODD action data is different
1)now how i can fix this porblem ?
2)how i can set interval for odd changes ? if this.props.odd.ad_time < nextProps.odd.ad_time change the odd in curenet props
json data for each odd :
{
"id": "24854545",
"home_od": "1.002",
"draw_od": "51.000",
"away_od": "51.000",
"ss": "3-1",
"time_str": "87",
"add_time": "1537706413"
}
reducer :
const initialState = {
odd: {}
};
function Markets(state = initialState, action) {
switch (action.type) {
case GET_ODD:
return {
...state,
...action.odd
};
default:
return state;
}
}
export default Markets
Markets action :
var marketsRESTRepository = new MarketsRESTRepository();
export const GET_ODD = "GET_ODD";
export function getOdd(odd) {
return {
type: GET_ODD,
odd
}
}
export function getAsyncOdd(eventId) {
return function (dispatch) {
marketsRESTRepository.getById(eventId).then(odd => {
dispatch(getOdd({odd}));
});
}
}
Action Odd data
same results for odds
in the market component try this :
import React, { Component } from "react";
import { connect } from 'react-redux';
import { getAsyncOdd } from "../../actions/markets.actions";
export class Markets extends Component {
constructor(props) {
super(props);
this.state = {
odds: {}
};
}
componentDidMount(){
this.props.getOdd(this.props.eventId);
this.setState({this.props.odd})
}
componentWillRecieveProps(nextProps) {
if(this.state.odds!==nextprops.odd){
this.setState({odds: nextprops.odd})
}
}
render() {
return (
<React.Fragment>
<span>{this.state.odds.home_od}</span>
<span>{this.state.odds.draw_od}</span>
<span>{this.state.odds.away_od}</span>
</React.Fragment>
);
}
}
function mapStateToProps({ markets }) {
return {
odd: markets.odd
}
}
function mapDispatchToProps(dispatch) {
return {
getOdd: (eventId) => dispatch(getAsyncOdd(eventId))
}
}
export default connect(mapStateToProps,
mapDispatchToProps
)(Markets)
Looks like the problem is with your reducer,
Why are you spreading the action? Since you are accessing it this way,
<span>{this.props.odd.home_od}</span>
Change it to this,
return {
...state,
add : action.odd
};
You have this:
dispatch(getOdd({odd}));
and the getOdd sync action will return this object:
{
type: "GET_ODD",
odd: {
odd: { /* data */}
}
}
which in turn will get dumped in the state by the reducer using the spread operator. I think you have to change the approach:
keep you getOdd action as is
when you call getOdd use it as getOdd(odd). No curly braces.
in you reducer, don't use the spread operator for the action. Use a specific key, for example:
return {
...state,
odd: action.odd
}
your async action looks good, just add a catch there and add a console.error so you know if something fails
For you 2 question. You can use one of the React life cycle methods to compare the current props and the next props coming and then perform that logic.
I hope it helps!

Object passed into Redux store is not reflecting all key/values after mapStateToProps

I have a component where toggle buttons are dynamically generated. Right now, I am just trying to get it working at a basic level so you click on a button and it adds a key/value pair to the cuts = {}.
After clicking on multiple buttons the cuts should have several key/value pairs: it does in the component where cuts resides, it does in the action, and it does in the Redux store via console.log(state.cuts).
However, after mapStateToProps it is only showing the first value and I am not sure why.
Anyway, here is my code and the flow as it is initiated by the user:
// bq_cuts.js component
constructor(props) {
super(props);
this.state = {
cuts: {}
}
}
onCutSelect(cut) {
const { bqResults } = this.props;
const { cuts } = this.state;
let key = cut.name;
let value = cut.value;
cuts[key] = value;
this.setState({
cuts
})
console.log(cuts); // shows all of the selected cuts here
bqResults(cuts);
}
// results.js actions
export function bqResults(results) {
console.log(results); // shows all of the selected cuts here
return function(dispatch) {
dispatch({
type: FILTER_RESULTS,
payload: results
})
}
}
// results.js reducer
import {
FILTER_RESULTS
} from '../actions/results';
export default function(state = {}, action) {
switch(action.type) {
case FILTER_RESULTS:
console.log(action.payload); //prints out all the cuts
return {
...state,
filter_results: action.payload
}
default:
return state;
}
return state;
}
const rootReducer = combineReducers({
results: resultsReducer,
});
export default rootReducer;
// bq_results.js component where the FILTER_RESULTS is accessed
render() {
console.log(this.props.filter_results); // only shows the first result
return (<div>...</div>)
}
function mapStateToProps(state) {
console.log(state.results.filter_results); // shows all selected cuts here
return {
filter_results: state.results.filter_results,
}
}
Maybe a better way of putting it is it seems like after the initial state is mapped to props, it is no longer receiving changes to state and mapping it to props.
Came across this article and used Approach #2:
https://medium.freecodecamp.org/handling-state-in-react-four-immutable-approaches-to-consider-d1f5c00249d5
Ended up with:
onCutSelect(cut) {
let cuts = {...this.state.cuts, [cut]: cut}
this.setState({
cuts
}, () => this.props.bqResults(this.state.cuts));
}

React/Redux and API data object

My app successfully gets API data and puts it to Redux state tree.
{
"coord":{
"lon":-0.13,
"lat":51.51
},
"weather":[
{
"id":311,
"main":"Drizzle",
"description":"drizzle rain",
"icon":"09d"
},
{
"id":501,
"main":"Rain",
"description":"moderate rain",
"icon":"10d"
}
],
//--------
//--------
"id":2643741,
"name":"London",
"cod":200
}
Props.data has been passed to components but in reality I have an access only to the
first key. For example props.data.name, props.data.id are accesiible. But props.data.coord.lon
and props.data.weather.map(---), are undefined.
Please, what's wrong with my understanding of using API dataset?
Component
export const DayItem = (props) => {
return (<MuiThemeProvider>
<Paper zDepth={2}>
{props.data.coord.lon} // No way!
{props.data.name} // OK!
</Paper>
</MuiThemeProvider>)}
Saga that gets data and dispatches an action. Puts data to Redux store.
function* getPosition() {
const getCurrentPosition = () => new Promise(
(res, rej) => navigator.geolocation.getCurrentPosition(res, rej))
// Gets user's current position assigned to const
const pos = yield call(getCurrentPosition);
const {latitude, longitude} = pos.coords;
// Yields the forecast API by user coordinates
const data = yield call(getForecastByCoords, latitude, longitude)
// Yields user's local forecast to the reducer
yield put({
type: LOAD_DATA_SUCCESS,
data
});
}
And mapStateToProps
function mapStateToProps(state) {
return {
chips: state.chipsReducer,
data: state.dataReducer
}
};
dataReducer
export const dataReducer = (state = {}, action) => {
switch (action.type) {
case LOAD_DATA_SUCCESS:
return action.data;
default:
return state;
}
};
Eventually, I got the point.
The problem was in the difference of speed React rendering vs Data loading.
Loading is always behind rendering. So, the complete set of data had no existence.
Just conditional rendering made my day {this.state.isLoading ? <div>Loading</div> : <DayItem {...this.props}/>}
you must use MapStateToProps, then use componentWillReceiveProps(nextProps)
Something like this
function mapStateToProps(state) {
return {
data:state.toJS().yourReducer.data,
}
then do next:
componentWillReceiveProps(nextProps) {
if (nextProps.data) {
if (nextProps.data.coord.lon != undefined) {
this.setState({yourData:nextProps.data.coord.lon}
}
}

Resources