I want to load data from api based on router params in component,
The channel page behave as expected when I first open the page, but if I go to other channel page by clicking, ChannelPage component didn't call componentDidMount, but reducer received FETCH_MESSAGES action, the Sidebar component also have the problem. redux-devtools can only received LOCATION_CHANGE action when other channel page get clicked. it's too weird!
What's the best practice for loading data based on params in react component?
Sidebar
class Sidebar extends React.Component {
componentDidMount() {
this.props.fetchChannels(1);
}
openChannelPage = (e, url) => {
e.preventDefault();
console.log('open channel page');
this.props.changeRoute(url);
};
render() {
let channelsContent = null;
if (this.props.channels !== false) {
channelsContent = this.props.channels.map((item, index) => (
<ChannelItem
routeParams={this.props.params} item={item} key={`item-${index}`} href={item.url}
handleRoute={(e) => this.openChannelPage(e, item.url)}
/>
), this);
}
return (
<div id="direct_messages">
<h2 className={styles.channels_header}>messages</h2>
<ul>
{channelsContent}
</ul>
</div>
);
}
}
const mapStateToProps = createStructuredSelector({
channels: selectChannels(),
});
const mapDispatchToProps = (dispatch) => ({
changeRoute: (url) => dispatch(push(url)),
fetchChannels: () => dispatch(fetchChannels()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Sidebar);
ChannelPage
class ChannelPage extends React.Component {
componentDidMount() {
console.log('##Fetch history messages##');
this.props.fetchMessages(this.props.channel);
}
render() {
return (
{this.props.channel.name}
);
}
}
const mapStateToProps = createStructuredSelector({
channel: selectChannel(),
});
export default connect(mapStateToProps, {
fetchMessages,
})(ChannelPage);
appReducer
function appReducer(state = initialState, action) {
switch (action.type) {
case RECEIVE_CHANNELS:
console.log('received channels');
return state;
case FETCH_CHANNELS:
console.log('fetching channels');
return state;
case FETCH_MESSAGES:
console.log('fetching history messages---WTF');
return state;
default:
return state;
}
}
export default appReducer;
reducer received unexpected FETCH_MESSAGES action
redux-devtools can only received LOCATION_CHANGE action when go to other channel page
Which one is the right behavior?
There are two questions
What's the best practice for loading data based on params in react component?
You can use componentDidUpdate() to dispatch request based on router params to get data from API
reducer received unexpected FETCH_MESSAGES action when navigate from messages/1 to messages/2
It's an expected behavior, reducer will receive previous action when navigate from messages/1 to messages/2, and the componentDidMount already ran once so it will never be called
Related
I'm building a web app using next.js & redux, and trying to fetch data from getInitialProps on server side.
There is a redux action ready to be triggered in getInitialProps and it gets triggered when I hit or refresh the page.
The problem is that after it gets triggered data get fetched from DB and stored in redux store successfully but the stored data never move to the page props so I cannot use the fetched data at all.
If I check "store.getState()" after the action triggered, there are data fetched from DB.
Funny thing is, if I click a button that triggers the same action, data get fetched and move to the page props so I can use them.
What do I do wrong here and how can I send the stored data to the page props automatically in the first place?
import Head from 'next/head'
import Link from 'next/link'
import { connect } from 'react-redux'
import { getItems } from '../../store/actions/itemAction'
const Index = props => {
return (
<>
<Head>
<title>Item List</title>
</Head>
<div>
<Button onClick={() => props.getItems()}>Get Items!</Button>
</div>
</>
)
}
Index.getInitialProps = async ({ store }) => {
await store.dispatch(getItems())
return {}
}
const mapStateToProps = state => ({
goods: state.item.goods,
})
const mapDispatchToProps = dispatch => ({
getItems: () => dispatch(getItems())
})
export default connect(mapStateToProps, mapDispatchToProps)(Index)
Here is the itemReducer.js
import { GET_ITEMS } from '../types'
const initialState = { goods: [], loading: false }
const itemReducer = (state = initialState, action) => {
switch (action.type) {
case GET_ITEMS:
return {
...state,
goods: action.payload,
loading: false,
}
default:
return state
}
}
export default itemReducer
First, getInitialProps() usually returns some page props. Then, did you know that mapStateToProps() has a second (optional) input parameter? See react-redux documentation.
const Index = ({ goods, getItems }) => (
<>
<Head>
<title>Item List</title>
</Head>
<div>{JSON.stringify(goods)}</div>
<div>
<Button onClick={() => getItems()}>Get Items!</Button>
</div>
</>
);
Index.getInitialProps = async ({ store }) => {
const goods = await store.dispatch(getItems());
return { goods };
};
const mapStateToProps = (state, ownProps) => ({
// At this point you could source your data either
// from `state.goods` or `ownProps` (which now has a `goods` prop).
// But sourcing it from the redux store means as soon as `goods`
// changes in the redux store, your component will re-render with
// the updated `goods`. A key aspect of `mapStateToProps()`.
goods: ...state.goods
});
const mapDispatchToProps = dispatch => ({
// I am assuming that dispatching this action will create
// a value in the redux store under the `goods` property
getItems: () => dispatch(getItems())
});
export default connect(mapStateToProps, mapDispatchToProps)(Index);
In my app component i have list of posts that contains user id, i want to display the user name and details against that user id, here's my app component's jsx:
App Component JSX:
render() {
const posts = [...someListOfPosts];
return posts.map((post) => {
return (
<div className="item" key={post.id}>
<div className="content">
<User userId={post.userId} />
</div>
</div>
);
});
}
User Component
import React from 'react';
import { connect } from 'react-redux';
import { fetchUser } from '../actions';
class UserHeader extends React.Component {
componentDidMount() {
this.props.fetchUser(this.props.userId); // getting correct userId
}
render() {
const { user } = this.props;
// Not displaying correct user i.e. showing the last resolved user for each post
return (
<div>
{user && <div className="header">{user.name}</div>}
</div>
);
}
}
const mapStateToProps = (state, props) => {
return {
user: state.user
};
}
export default connect(mapStateToProps, { fetchUser })(UserHeader);
I'm getting correct props for userId but for every post it displays the last resolved user from the api. It should be relevant user for every post.
Reducer and Action Creator
// action
export const fetchUser = (id) => {
return async (dispatch) => {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
dispatch({
type: 'FETCH_USER',
payload: (response.status === 200 && response.data) ? response.data : null; // it returns single user not array of user
});
}
}
// reducer
export default (state = null, action) => {
switch (action.type) {
case 'FETCH_USER':
return action.payload; // i know it can be fixed by defaulting state to empty array and returning like so [...state, action.payload] but why should i return complete state why not just a single user object here?
default:
return state;
}
}
The fetchUser action creator returns single payload of a user not an array then why it's required to return the state like [...state, action.payload] why can't it be done by returning action.payload only? I've tried it by returning only action.payload but in my user component it displays the last resolved user from the api every time for each post. I'm confused regarding this.
You are subscribing to the store using mapStateToProps which rerenders when ever there is a change in the store. As you are trying to render via props in User component, the application retains the last value of user and re-renders all the old User Components as well. If you want to ignore the props updates make the result local to the component.
You can possibly try this:
import React from 'react';
import { connect } from 'react-redux';
import { fetchUser } from '../actions';
class UserHeader extends React.Component {
constructor(props){
super(props);
this.state={
userDetails:{}
}
}
componentDidMount() {
fetch(https://jsonplaceholder.typicode.com/users/${this.props.userId})
.then(res => res.json())
.then(
(result) => {
this.setState({
userDetails: result.data
});
},
// 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: false
});
}
)
}
render() {
return (
<div>
{this.state.userDetails && <div className="header">{this.state.userDetails.name}</div>}
</div>
);
}
}
const mapStateToProps = (state, props) => {
return {
};
}
export default connect(mapStateToProps, { fetchUser })(UserHeader);
What is the best way to trigger an action inside componentDidMount () using a redux props? ex:
import { fetchUser } from '../actions'
class Example extends Component {
ComponentDidMount(){
this.props.fetchUser(this.props.id)
} ...
mapDispatchToProps = dispatch => ({
fetchUser: (payload) => dispatch(fetchUser(payload))
})
mapStateToProps = state => ({
id: state.user.id
})
The problem is that ComponentDidMount () is mounted before the class even receives props from the store. That way my this.props.id is = 'undefined' inside the method.
One solution I found was to run as follows but I do not know if it's the best way:
import { fetchUser } from '../actions'
class Example extends Component {
fetchUser = () => {
this.props.fetchUser(this.props.id)
}
render(){
if(this.props.id !== undefined) this.fetchUser()
} ...
}
mapDispatchToProps = dispatch => ({
fetchUser: (payload) => dispatch(fetchUser(payload))
})
mapStateToProps = state => ({
id: state.user.id
})
That way I get the requisition, but I do not think it's the best way. Any suggestion?
Have you tried using async/await?
async ComponentDidMount(){
await this.props.fetchUser(this.props.id)
} ...
You have to understand the lifecycle of react components. When the component gets mounted, it can fetch data, but your component at that point needs something to render. If the data hasn't been loaded yet, you should either return null to tell react that it's not rendering anything at that point, or perhaps a loading indicator to show that it's fetching data?
import { fetchUser } from '../actions'
class Example extends Component {
componentDidMount() {
this.props.fetchUser();
}
render(){
const { loading, error, user } = this.props;
if (loading) {
return <LoadingIndicator />;
}
if (error) {
return <div>Oh noes, we have an error: {error}</div>;
}
// Render your component normally
return <div>{user.name}</div>;
}
}
Your reducer should have loading set to true by default, and when your fetch completes, set loading to false, and either set the user or error depending on if the fetch fails/completes.
I have a component PostsShow which is showing the selected post:
#connect((state) => ({post: state.posts.post}), {fetchPost})
class PostsShow extends Component {
componentDidMount() {
this.props.fetchPost(this.props.match.params.id);
}
render() {
const { post } = this.props;
if (!post) {
return <div></div>;
}
return (
<div>
<Link to='/'>Back</Link>
<h3>{post.title}</h3>
<h6>Categories: {post.categories}</h6>
<p>{post.content}</p>
</div>
);
}
}
The problem is when user first visits the page, fetchPost function populates state section (posts.post) with some data associated with chosen post and when the user chooses another post, he can see old data for 1-2 sec. (before new request is finished).
Actions map:
Click on post #1
Click Back button
Click on post #2
For 1 sec. you can see old (#1) post, until the request is finished and component refreshed with the post (#2) data.
I'm new to whole redux concept, so i'm curious how are you avoiding this kind of behavior?
MY SOLUTION:
I assume that you can create a switch branch, which will modify sate with (posts.post part) with null value and trigger this behavior on componentWillUnmount method. So:
Action:
export function clearPost() {
return {
type: CLEAR_POST,
payload: null
}
}
Reducer:
const INITIAL_STATE = { all: [], post: null };
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
// ... Other cases
case CLEAR_POST:
return { ...state, post: null }
default:
return state;
}
}
Component:
class PostsShow extends Component {
componentDidMount() {
this.props.fetchPost(this.props.match.params.id);
}
componentWillUnmount() {
this.props.clearPost();
}
render() {
// Old render
}
}
Is this a good approach for react with redux?
Your state structure is not ideal. Try keeping your posts like that:
posts: {
byID: {
1: {/*the post* no 1/},
2: {/*the post* no 2/},
// ...
}
allIDs: [2, 1 /*, ...*/],
}
This way you can provide an ordered list of post id's for a list view and show a single post by getting it from the state like: this.posts.byID['thePostID'].
Also read up in the redux docs on how to normalize state.
This will also fix your problem because when your get your post from the store with an id that does not already exist, it will be undefined thus rendering as an empty div. A loading indicator would be the best thing to show.
This is because you are making an async call , which takes time to get new date, that is why when fetch posts gets new data then your component will update and render it.
You can have two approaches:
1) you can create a an async function which will dispatch an action that will have a update the reducer with payload as
status :LOADING,
once the fetch function returns with new data again dispatch an action that
will update the reducer as
status:SUCCESS
and in your component check if status received in the props by store is 'LOADING' or 'SUCCESS', if SUCCESS then show new state, if LOADING keep showing some text like component is Loading:
here is the example
SomeComponent.js
export default class SomeComponent extends Component {
constructor (props) {
super(props)
this.model = {}
}
render () {
const {
status
} = this.props
const loading = status === 'LOADING'
return (
<Layout>
<Row>
<FormField
label='First Name'
id='first-name'
value={details.firstName}
onChange={this.update('firstName’)}
disabled={loading}
mandatory
/>
</Row>
</Layout>
)
}
}
actions.js
const fetchDetails = (someId) => {
return (dispatch) => {
dispatch(dataFetching())
return http.get(dispatch, `https//someCall`, null, {
'Accept': SOME_HEADER
}).then((data) => {
dispatch(dataFetched(data))
}).catch((error) => {
dispatch(someError(`Unable to retrieve the details ${error}`))
})
}
}
const dataFetching = () => ({
type: constants.DATA_FETCHING
})
const dataFetched = (data) => ({
type: constants.DATA_FETCHED,
data
})
Reducer.js
export default function reducer (state = {}, action = {}) {
switch (action.type) {
case constants.DATA_FETCHING:
return Object.assign({}, state, {
data: {},
status: 'LOADING'
})
case constants.DATA_FETCHED:
return Object.assign({}, state, {
data: action.data,
status: 'SUCCESS'
})
default:
return state
}
}
2nd Approach
The one which you did, but for clarity try to use the first one.
I am new to react native + redux. I have an react native application where user first screen is login and after login am showing page of list of categories from server. To fetch list of categories need to pass authentication token, which we gets from login screen or either if he logged in previously then from AsyncStorage.
So before redering any component, I am creating store and manully dispatching fetchProfile() Action like this.
const store = createStore(reducer);
store.dispatch(fetchProfile());
So fetchProfile() try to reads profile data from AsyncStorage and dispatch action with data.
export function fetchProfile() {
return dispatch => {
AsyncStorage.getItem('#myapp:profile')
.then((profileString) => {
dispatch({
type: 'FETCH_PROFILE',
profile: profileString ? JSON.parse(profileString) : {}
})
})
}
}
so before store get populated, login page get rendered. So using react-redux's connect method I am subscribing to store changes and loading login page conditionally.
class MyApp extends React.Component {
render() {
if(this.props.profile)
if(this.props.profile.authentication_token)
retunr (<Home />);
else
return (<Login />);
else
return (<Loading />);
}
}
import { connect } from 'react-redux';
const mapStateToProps = (state) => {
return {
profile: state.profile
}
}
module.exports = connect(mapStateToProps, null)(MyApp);
So first 'Loading' component get rendered and when store is populated then either 'Login' or 'Home' component get rendered. So is it a correct flow? Or is there a way where I can get store populated first before any compnent render and instead of rendering 'Loading' component I can directly render 'Login' or 'Home' Component.
Verry common approach is to have 3 actions for an async operation
types.js
export const FETCH_PROFILE_REQUEST = 'FETCH_PROFILE_REQUEST';
export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
export const FETCH_PROFILE_FAIL = 'FETCH_PROFILE_FAIL';
actions.js
import * as types from './types';
export function fetchProfile() {
return dispatch => {
dispatch({
type: types.FETCH_PROFILE_REQUEST
});
AsyncStorage.getItem('#myapp:profile')
.then((profileString) => {
dispatch({
type: types.FETCH_PROFILE_SUCCESS,
data: profileString ? JSON.parse(profileString) : {}
});
})
.catch(error => {
dispatch({
type: types.FETCH_PROFILE_ERROR,
error
});
});
};
}
reducer.js
import {combineReducers} from 'redux';
import * as types from './types';
const isFetching = (state = false, action) => {
switch (action.type) {
case types.FETCH_PROFILE_REQUEST:
return true;
case types.FETCH_PROFILE_SUCCESS:
case types.FETCH_PROFILE_FAIL:
return false;
default:
return state;
}
};
const data = (state = {}, action) => {
switch (action.type) {
case types.FETCH_PROFILE_SUCCESS:
return action.data;
}
return state;
};
export default combineReducers({
isFetching,
data
});
So you can get isFetching prop in your component and show/hide Loader component
You can load all your data during the splash screen and then load the others screens after that. I did it like this. Hope it helps
class Root extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
store: configureStore( async () => {
const user = this.state.store.getState().user || null;
if (categories && categories.list.length < 1) {
this.state.store.dispatch(categoriesAction());
}
this.setState({
isLoading: false
});
}, initialState)
};
}
render() {
if (this.state.isLoading) {
return <SplashScreen/>;
}
return (
<Provider store={this.state.store}>
<AppWithNavigationState />
</Provider>
);
}
}
Redux and Redux Persist (https://github.com/rt2zz/redux-persist) will solve your problem.
Don't make them complex.