In a very conventional pattern, you have a list of posts at /posts and a detail view at /posts/1. If you have an API server on the backend and React+Redux on the frontend, you probably fetch resources when you reach /posts. But what do you do when you reach /posts/1? If you landed /posts/ first, you already have all the resources, so you can maybe do the following:
Have posts reducer that returns all the posts we fetched from API server
Have currentPost reducer that returns only the relevant post
To set currentPost you can dispatch an action that updates currentPost as soon as you click a post in the index view.
But if you landed /posts/1 without going to the index page, or refreshed /posts/1, you don't have the resources that you load in the index page (i.e. posts reducer returns []). To solve this, you can request /posts/1 from the API server and set currentPost then.
Question: Did I understand the flow correctly? I am not sure if currentPost reducer is even necessary. Also, I am not sure if it's conventional to use the resources from the index page and request a single resource only when it's necessary.
If you get all data you need to display currentPost in /posts request, will be sufficient to have one reducer to avoid duplicating items.
In postsReducer you need to handle two actions:
1. When you get all posts from server, your reducer should return them.
2. When you get a particular post, just append him to all posts list and return resulting array.
//reducers.js
function postsReducer(state = [], action) {
switch (action.type) {
case 'RECEIVE_POSTS':
return action.posts;
case 'RECEIVE_POST':
return [...state, action.post]
default:
return state;
}
}
PostContainer should dispatch an action to fetch currentPost. When currentPost post data will be fetched from server, you can pass it to your Post presentational component.
// PostContainer.js
class PostContainer extends Component {
componentWillMount() {
if (!this.props.post) {
this.props.dispatch(loadPost(this.props.params.id));
};
}
render() {
return (
<Post post={this.props.post}
);
}
}
function mapStateToProps(state, ownProps) {
// if you're using react-router, post id will be in `params` property.
const id = ownProps.params.id;
return {
post: state.posts.filter(post => post.id === id)[0]
};
};
export default connect(mapStateToProps)(PostContainer);
PostsListContainer should dispatch action to fetch all posts from server. When request finished successfully, you will pass array with posts to PostsList component.
// PostsListContainer.js
class PostsListContainer extends Component {
componentWillMount() {
if (!this.props.posts) {
this.props.dispatch(loadPosts());
}
}
render() {
return (
<PostsList posts={this.props.posts}
);
}
}
function mapStateToProps(state) {
return {
posts: state.posts
}
};
export default connect(mapStateToProps)(PostsListContainer);
A practical approach would be store all posts and request the missing ones. Supposing your posts reducer is something like this:
function posts(state = {}, action) {
switch (action.type) {
case "FETCH_ALL_POSTS":
return {...state, ...action.posts}
case "FETCH_POST":
return {...state, [action.post.id]: action.post}
default:
return state
}
}
You could define 2 actions:
One for requesting all posts (it may include pagination params)
One for requesting a single post
// Fetch all posts.
//
// In this example we are expecting the response to be like:
//
// {
// 12: {id: 12, title: "Hello", content: "..."},
// 16: {id: 16, title: "Bonjour", content: "..."},
// 54: {id: 54, title: "Hola", content: "..."},
// ...
// }
//
// If you want to return an array instead of a map the you need
// to normalize `posts`.
//
function fetchAllPosts() {
return dispatch => {
fetch("/api/posts")
.then(res => res.json())
.then(posts => dispatch({type: "FETCH_ALL_POSTS", posts}))
}
}
// Fetch a single post.
//
// Response would be:
//
// {id: 12, title: "Hello", content: "..."}
//
function fetchPost(id) {
return (dispatch, getState) => {
const state = getState()
// Check if the post is cached
if (state.posts[id]) {
dispatch({type: "FETCH_POST", post: state.posts[id]})
}
// Otherwise we must query the API
fetch(`/api/post/${id}`)
.then(res => res.json())
.then(post => dispatch({type: "FETCH_POST", post}))
}
}
Then in your components (before mounting them or after the routing) you can call the above action to trigger the loading. Let's consider you want to show the list of posts:
const PostList = connect(
state => ({
// Retrieve all posts as an array
posts: Object.values(state.posts),
}),
dispatch => ({
fetchAllPosts: () => dispatch(fetchAllPosts()),
})
)(
class PostList extends Component {
componentWillMount() {
// Load all posts if none were stored
if (this.props.posts.length === 0) {
this.props.fetchAllPosts()
}
}
render() {
return (
<ul>
{this.props.posts.map(
post => <PostItem key={post.id} id={post.id} />
)}
</ul>
)
}
}
)
const PostItem = connect(
(_, initialProps) => {
return state => ({
// Get the post data
post: state.posts[initialProps.id],
})
}
)(
class PostItem extends Component {
render() {
return (
<li>{this.props.post.title}</li>
)
}
}
)
Tada! The simple case is handled. Now if we want to display a single post, we read it from the store, or fetch it.
const PostDetails = connect(
(_, initialProps) => {
// Read the post ID from the initial properties.
// We could imagine another case where the ID is read from the location.
const {id} = initialProps
return state => {
// May, or may not, return a post
post: state.posts[id],
}
},
(dispatch, initialProps) => {
// Same as above, we need to retrieve the post ID.
const {id} = initialProps
// Prepare an action creator to load THIS post.
const fetchThisPost = () => {
dispatch(fetchPost(id))
}
return () => ({
fetchThisPost,
})
}
)(
class PostDetails extends Component {
componentWillMount() {
// Load this post if it is not cached
if (!this.props.post) {
this.props.fetchThisPost()
}
}
render() {
if (!this.props.post) {
return <Loading />
} else {
return <PostCard />
}
}
}
)
Related
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);
I have a React component named ItemList which loads a list of items from an API server and then renders them as a list of Item components.
Each Item has a delete button. When the button is clicked, I want to send a request to the API server to delete the item, then re-render the ItemList.
One way I can think of to make that work is to move both API queries into reducers, and then dispatch actions whenever I want to a) get all items; and b) delete an item. Upon successful completion of those API operations, the reducer will update the store and the ItemList will re-render.
Is that a reasonable approach, or is it a bad idea to put API calls inside of reducers?
Here's a simplified version of the the code I have so far. It doesn't yet use Redux. I want to make sure my approach is sound before implementing Redux, hence this Stack Overflow question.
ItemList.js
class ItemList extends Component {
constructor(props) {
super(props);
this.state = {
items: []
};
this.componentDidMount = this.componentDidMount.bind(this);
}
componentDidMount() {
const url = 'https://api.example.com/api/v1.0/item';
fetch(url, {
method: "get"
})
.then(res => res.json())
.then(response => {
this.setState({items: response.data});
});
}
render() {
<div>
{this.state.items.map((item, index) => (
<Item key={item.id} item={item} />
))}
</div>
}
}
Item.js
class Item extends Component {
deleteClicked() {
/**
* Is it ok to dispatch a "delete item" action
* from here and then make the actual API call
* in a reducer?
*/
}
render() {
<div>
<h2>{item.title}</h2>
<a onClick={this.deleteClicked}>delete item</a>
</div>
}
}
You're almost solved your task. To make you solution perfect use Action creators to make async calls and dispatch actions on completion. Reducer should be pure sync function.
For example ItemList component may use such action creator to extract items
const ExtractItemsAction = () => (dispatch) => {
dispatch ({type: ITEMS_REQUESTED});
const url = 'https://api.example.com/api/v1.0/item';
fetch(url, {
method: "get"
})
.then(res => res.json())
.then(response => {
dispatch({type: ITEMS_RECEIVED, items: response.data});
});
}
And reducer will stay pure
function reducer (state = initalState, action)
{
switch (action.type) {
case ITEMS_REQUESTED:
return { ...state, itemsRequested: true }
case ITEMS_RECEIVED:
return { ...state, itemsRequested: false, items: action.items }
default
return state;
}
}
And don't forget to connect you component to Redux, and use Redux-thunk as middleware when creating store.
I have a simple form in react-redux meant to try to add a user to the database, if it is successful, display a success message. However I am not sure of the best approach to do this. I have the following:
onSubmit = e => {
...
const newUser = { user object here }
this.props.registerUser(newUser);
}
in componentWillReceiveProps(nextProps):
if (nextProps.success === true) {
this.setState({ success: nextProps.success });
}
in the render():
Meant to display a success component giving further information. There is also a conditional check to hide the form if success is true
{ this.state.success === true &&
(<SuccessComponent name={this.state.name} />)
}
in mapStateToProps:
const mapStateToProps = state => ({
success: state.success
});
in my action:
.then(res => {
dispatch({
type: REGISTRATION_SUCCESS,
payload: true
});
})
in the reducer:
const initialState = false;
export default function(state = initialState, action) {
switch (action.type) {
case REGISTRATION_SUCCESS:
return action.payload;
default:
return state;
}
}
in combineReducers:
export default combineReducers({
success: successReducer
});
In this, I am basically using the response from the server to dispatch a success prop to the component, update the state, which forces react to render and go through the conditional statement again, hiding the form and displaying the new success block.
However, when I go into redux dev tools, I see that the state from the store is now true, and remains so should users navigate away. Is there a better way to go about this objective? I find that maybe this should be isolated to component state itself, but not sure how to do it since the action and hence the server response is through redux.
Redux is a state machine, not a message bus, so try to make your state values represent the current state of the application, not to send one-time messages. Those can by the return value of the action creator. Success or failure can simply be the existence/lack of an error from the action creator.
If you actually do want to store the user info, you can derive your "was successful" state by virtue of having a registered user, and clear out any existing registered user on component mount.
// actions.js
export const clearRegisteredUser = () => ({
type: SET_REGISTERED_USER,
payload: null,
})
export const register = (userData) => async (dispatch) => {
// async functions implicitly return a promise, but
// you could return it at the end if you use the .then syntax
const registeredUser = await api.registerUser(userData)
dispatch({
type: SET_REGISTERED_USER,
payload: registeredUser,
})
}
// reducer.js
const initialState = { registeredUser: null }
const reducer = (state = initialState, { type, payload }) => {
switch(type) {
case SET_REGISTERED_USER: {
return {
...state,
registeredUser: payload,
}
}
default: {
return state
}
}
}
// TestComponent.js
class ExampleComponent extends Component {
state = {
registrationError: null,
}
componentWillMount() {
this.props.clearRegistered()
}
handleSubmit = async (formData) => {
try {
this.props.register(formData)
} catch(error) {
// doesn't really change application state, only
// temporary form state, so local state is fine
this.setState({ registrationError: error.message })
}
}
render() {
const { registrationError } = this.state
const { registeredUser } = this.props
if (registrationError) {
return <FancyError>{registrationError}</FancyError>
} else if (registeredUser) {
return <SuccessComponent name={this.props.registeredUser.name} />
} else {
return <UserForm onSubmit={this.handleSubmit} />
}
}
}
If you really don't need the user info after you register, you can just perform the api call directly and leave Redux out of it.
class ExampleComponent extends Component {
state = {
registered: false,
registrationError: null,
}
handleSubmit = async (formData) => {
try {
await api.registerUser(formData)
this.setState({ registered: true })
} catch(error) {
this.setState({ registrationError: error.message })
}
}
render() {
const { registered, registrationError } = this.state
if (registrationError) {
return <FancyError>{registrationError}</FancyError>
} else if (registered) {
return <SuccessComponent name={this.props.registeredUser.name} />
} else {
return <UserForm onSubmit={this.handleSubmit} />
}
}
}
Finally, avoid keeping copies of redux state in your component state whenever possible. It can easily lead to sync issues. Here's a good article on avoiding derived state: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
First off, I don't think that you should be using your Redux store for saving what is essentially local state.
If it was me I would probably try to make the api call directly from the component and then write to the redux store if it is successful. That way you could avoid having your derived state in the component.
That said, if you want to do it this way I would suggest componentWillUnmount. That would allow you have another Redux call that would turn your registration boolean back to false when you leave the page.
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.
When I try to dispatch an action from inside an ajax call, while the store itself gets updated (Checked in the browser redux plugin), the component doesn't receive the event, and therefore doesn't re-render.
class SomeComponent extends React.Component{
constuctor (props) {
super(props);
this.someAjaxCall();
}
someAjaxCall(){
this.props.dispatch(someAction('Work'));
$.ajax({
type: "POST",
url: "someURL",
success: function(data)
{
this.props.dispatch(someAction("Does Not Work"));
}.bind(this)
});
}
render(){
return (<div></div>);
}
}
const mapStateToProps = (store, ownprops) => {
return {
store: store.get("Some_Key")
}
}
const render = (store,container) => {
let ConnectedComponent = ReactRedux.connect(this.mapStateToProps)(SomeComponent);
ReactDOM.render(
<Provider store={ store }>
<ConnectedComponent/>
</Provider>
, container);
}
const someAction = (state) => {
return {
type: 'SOME_CASE',
state: state
}
}
const reducer = (state, action) => {
switch (action.type) {
case 'SOME_CASE':{
return state.set("stateKey",action.state);
}
default: {
return state;
}
}
}
From the above sample, when I call this.props.dispatch(someAction('Work'));
The store gets updated, the component gets the event and re-renders itself. However, when I call from inside the 'success' function of an ajax call, the store gets updated, but the component never receives the event.
If I am wrong, please correct me.
The second Action creator call should not be failed as someAction is not global. Did you get any errors.