For a react application, I'm using Redux to fetch data from an API. In this application, there exists a component that is displayed twice on the same page. The component is connected to an action and reducer.
Both instances of the component should display different data: one displays someone's job and the other displays someone's phone number. Both of these things are requested in separate API calls, causing the problem that the data of the second call, overwrites the data obtained in the first call in the reducer connected to the component. How would it be possible to make two API calls for such a component that is shown twice, such that both instances of it show the data of either one of these api calls?
I tried the following: make one request and fill the reducer. Then make the other request and merge the results of both in the reducer. However, the problem with this approach is that it is also possible to display only one of the components in the application.
This is the react component:
class Display extends React.Component {
componentWillMount() {
const { fetchpayload } = this.props;
fetchpayload(this.props.parameter);
}
render() {
return (
<h1>{this.props.payload}</h1>
);
}
}
const mapStateToProps = (state) => ({
payload: state.DisplayReducer.payload,
});
const mapDispatchToProps = (dispatch) => bindActionCreators(
{
fetchpayload: payloadAction,
},
dispatch,
);
This is the reducer
const initialState = {
payload: [],
};
export function DisplayReducer(state = initialState, action) {
switch (action.type) {
case 'FETCHED_DISPLAY':
return {
...state,
payload: action.payload,
};
return state;
}
}
The action file makes the request, and dispatches 'FETCHED_DISPLAY'.
just don't override your stuff inside reducer
const InitialState = {
person: {
name: '',
job: ''
}
};
export default (state = InitialState, action) => {
switch(action.type) {
case NAME:
return {
...state.person,
person: {
job: ...state.person.job,
name: action.payload
}
}
case JOB:
return {
...state.person,
person: {
name: ...state.person.name,
job: action.payload
}
}
// do that with whatever property of object person you would want to update/fetch
}
}
}
Hope this helps.
Related
I have been trying to add the functionality of adding favourites in the movie app I created by watching a tutorial about Redux, but for some reason the state is always empty.
This is what my action and reducers look like
Reducers
const initState = {
favourites: [],
};
const favouriteReducer = (state = initState, action) => {
switch (action.type) {
case "ADD_FAVOURITE":
return {
...state,
favourites: action.payload.favourites,
};
default:
return { ...state };
}
};
export default favouriteReducer;
actions
export const favouriteActions = () => {
return {
type: "ADD_FAVOURITE",
payload: "zz",
};
};
The action is dispatched and showed in the redux dev tools too but nothing is added to the favorite state which I have created.
I have this onclick event set to an image of the star on which I actually want to pass in the Id of the movie which I have access to from another state.
const addFav = () => {
dispatch(favouriteActions(id));
};
reutrn{
<img src={favNot} onClick={addFav} />
}
I am just posting the main part of my component file here. I have also attached an Image showing my current state after I click in the image.
return {
...state,
favourites: action.payload.favourites,
};
Just only need "action.payload" or if you want favourites like array should change reducer : favourites : [...state.favourites,action.payload]
You are using
case "ADD_FAVOURITE":
in reducer but in action the type is
type: "ADD_FAVOURITES",
Could this be the cause?
export const favouriteActions = () => {
return {
type: "ADD_FAVOURITE",
favourites: "zz",
};
};
Please check whether above works. Instead of "playload" change to "favourites"
I'm using the Redux Thunk example template. When I dispatch an action in getInitialProps, that populates my store, the data is loaded but after the page is rendered, the store is still empty.
static async getInitialProps({ reduxStore }) {
await reduxStore.dispatch(fetchCategories())
const categories = reduxStore.getState().programm.categories;
console.log('STATE!!!', categories)
return { categories }
}
The categories will load correctly but when I inspect my store, the categories state is empty.
Here is my store:
import db from '../../api/db'
// TYPES
export const actionTypes = {
FETCH_PROGRAMMS: 'FETCH_PROGRAMMS',
FETCH_CATEGORIES: 'FETCH_CATEGORIES'
}
// ACTIONS
export const fetchCategories = () => async dispatch => {
const categories = await db.fetchCategories();
console.log('loaded Cate', categories)
return dispatch({
type: actionTypes.FETCH_CATEGORIES,
payload: categories
})
}
// REDUCERS
const initialState = {
programms: [],
categories: []
}
export const programmReducers = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_PROGRAMMS:
return Object.assign({}, state, {
programms: action.payload
})
case actionTypes.FETCH_CATEGORIES:
console.log('Payload!', action);
return Object.assign({}, state, {
categories: action.payload
})
default:
return state
}
}
How can I make the redux state loaded on the server (in getInitialProps) be carried over to the client?
After hours of searching for solution it seems like I found my problem. It seems like I need to pass an initialState when creating the store. So instead of this:
export function initializeStore() {
return createStore(
rootReducers,
composeWithDevTools(applyMiddleware(...middleware))
)
}
I'm doing this and it works now
const exampleInitialState = {}
export function initializeStore(initialState = exampleInitialState) {
return createStore(
rootReducers,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
)
}
If you do this:
return { categories }
in getInitialProps, categories should be available in component's props in client side.
It should be available in Redux as well, this could cause the problem:
return Object.assign({}, state, {
categories: action.payload
})
Take a look at this Object.assign, the function only takes 2 parameters.
My normal way of doing this:
return {
...state,
categories: action.payload,
};
I'm having trouble understanding how the redux state assigns the state objects based on the action payload and the reducer functions. Below is my sample code. I've made notes and asked questions along the different sections, but in summary these are my questions:
Why does Option 2 below not work?
Why do I have to map my state to my competitionList prop using state.competitions and not state.items?
Any resources to get a good grasp of how react and redux connect and mapping functions work. I've already gone through the official docs and done some googling, but perhaps someone has a reference that they found easier to understand all the different options and ways of mapping state and dispatch.
My Action code:
function getAll() {
return dispatch => {
dispatch(request());
myService.getAll()
.then(
competitions => dispatch(success(competitions)),
error => dispatch(failure(error))
);
};
function request() { return { type: constants.GETALL_REQUEST } }
function success(competitions) { return {type: constants.GETALL_SUCCESS, competitions}}
function failure(error) { return {type: constants.GETALL_FAILURE, error}}
}
My reducer code:
import { constants } from '../_constants';
const initialState = {items: [], loading: false, selected: null}
export function competitions(state = initialState, action) {
switch (action.type) {
case constants.GETALL_REQUEST:
return {
loading: true
};
case constants.GETALL_SUCCESS:
console.log("the action value: ", action)
return {
items: action.competitions
};
case constants.GETALL_FAILURE:
console.log("the failed action value: ", action)
return {
error: action.error
};
default:
return state
}
}
In my component I have a mapStateToProp function which I pass to connect. The first one does not work. Why?
Option 1 - Not working
function mapStateToProps(state) {
const { selected, ...competitions } = state.competitions;
return {
competitionList: competitions,
isLoading: state.loading
};
}
export default connect(mapStateToProps)(Dashboard);
This one works, but I would like the competitionList variable to have the returned items array instead of the whole state object, so I tried to do something like this competition: state.competitions.items but it raises an error.
Option 2 - Partially working (I want to only assign the competition items)
const mapStateToProps = (state) => ({
competitionList: state.competitions,
isLoading: state.loading
});
export default connect(mapStateToProps)(Dashboard);
I cannot do:
const { competitionList } = this.props;
{competitionList.map(competition =>
<tr key={competition.competitionId}>
<td>{competition.competitionName}</td>
</tr>
)}
I have to do:
const { competitionList } = this.props;
{competitionList.items.map(competition =>
<tr key={competition.competitionId}>
<td>{competition.competitionName}</td>
</tr>
)}
I think the point that you are missing is when you combine your reducers, each one will have a key because they are objects.
In the file you combine your reducers, you probably have something like that:
import { combineReducers } from 'redux'
import todos from './todos'
import competitions from './competitions'
export default combineReducers({
todos,
competitions
})
After that, your state will look like this:
{
todos:{},
competitions:{
items: [],
loading: false,
selected: null
}
}
Explained that I think everything will be easier.
Option 1 - Not working: It is not working because you don't havecompetitions attribute inside the competitions state. Even if you have, you should not use the ... before it. If you replace the competitions for items, it is going to work, because items are inside the competitions state:
function mapStateToProps(state) {
const { selected, items } = state.competitions;
return {
competitionList: items,
isLoading: state.loading
};
}
export default connect(mapStateToProps)(Dashboard);
Or we can improve it, to make it shorter:
function mapStateToProps(state) {
const { selected, items } = state.competitions;
return {
items,
selected
isLoading: state.loading
};
}
export default connect(mapStateToProps)(Dashboard);
Doing this way, you can use this part of your code:
const { items } = this.props;
{items.map(competition =>
<tr key={competition.competitionId}>
<td>{competition.competitionName}</td>
</tr>
)}
There is another point I would like to point, Probably your isLoading variable is not working either, because you are trying to read it directly from the state, instead of from a reducer in the state.
Edited: I missed another point. Your reducer always has to return the whole state instead of just an attribute of it.
import { constants } from '../_constants';
const initialState = {items: [], loading: false, selected: null, error: null}
export function competitions(state = initialState, action) {
switch (action.type) {
case constants.GETALL_REQUEST:
/*return {
loading: true
};*/
//returning that I will overwrite your competition state with this object.
// this will keep all the competition state and will gerenate a new object changing only the loading attribute
return {
...state,
loading:true
}
case constants.GETALL_SUCCESS:
console.log("the action value: ", action)
return {
...state,
items: action.competitions
};
case constants.GETALL_FAILURE:
console.log("the failed action value: ", action)
return {
...state,
error: action.error
};
default:
return state
}
}
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'm quite new to Redux and from what I understand, a reducer should be created for each type of object. E.g. for user interaction a user reducer should be created. My question is: How do you handle cases where you require the object for different purposes?
Scenario: Imagine having a user reducer which returns the current user. This user would be required in the entire application and needed for general controls on every page.
Now what happens when you need to load another user which is used for different purposes. E.g. profile page: loading a user to display information.
In this case there would be a conflict if the user reducer would be used. What would be the correct way to handle this in Redux? In case a different reducer would have to be created, what would be the naming convention for the new reducer?
First, you've mentioned:
a user reducer which loads the current user
I don't know if I got you correctly, but if this means you want to fetch (from an API, for example) the current user inside the reducer, this is a wrong approach.
Reducers are intended to be pure functions. You can call them with the same arguments multiple times and they will always return the same expected state.
Side effects like that should be handled by action creators, for example:
actions/user.js
export const FETCH_ME = 'FETCH_ME'
export const FETCH_ME_SUCCESS = 'FETCH_ME_SUCCESS'
// it's using redux-thunk (withExtraArgument: api) module to make an async action creator
export const fetchMe = () => (dispatch, getState, api) => {
dispatch({ type: FETCH_ME })
return api.get('/users/me').then(({ data }) => {
dispatch({ type: FETCH_ME_SUCCESS, data })
return data
})
}
Inside your reducer you can simple get the data and set a new state (note that if you send the action with the same data multiple times, the state will always be the same).
reducers/user.js
import { FETCH_ME, FETCH_ME_SUCCESS } from '../actions/user'
const initialState = {
item: null,
loading: false
}
export const userReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_ME:
return {
...state,
loading: true
}
case FETCH_ME_SUCCESS:
return {
...state,
loading: false,
item: action.data
}
default:
return state
}
}
Now, for your scenario:
Now what happens when you need to load another user which is used for different purposes. E.g. profile page: loading a user to display information.
You will just write another action creator for that:
actions/user.js
export const FETCH_ME = 'FETCH_ME'
export const FETCH_ME_SUCCESS = 'FETCH_ME_SUCCESS'
export const FETCH_USER = 'FETCH_USER'
export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS'
export const fetchMe = () => (dispatch, getState, api) => {
dispatch({ type: FETCH_ME })
return api.get('/users/me').then(({ data }) => {
dispatch({ type: FETCH_ME_SUCCESS, data })
return data
})
}
export const fetchUser = (id) => (dispatch, getState, api) => {
dispatch({ type: FETCH_USER })
return api.get(`/users/${id}`).then(({ data }) => {
dispatch({ type: FETCH_USER_SUCCESS, data })
return data
})
}
Then you adapt your reducer to manage more sets:
reducers/user.js
import { combineReducers } from 'redux'
import { FETCH_ME, FETCH_ME_SUCCESS, FETCH_USER, FETCH_USER_SUCCESS } from '../actions/user'
const initialState = {
item: null,
loading: false
}
const meReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_ME:
case FETCH_ME_SUCCESS:
return userReducer(state, action)
default:
return state
}
}
const activeReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USER:
case FETCH_USER_SUCCESS:
return userReducer(state, action)
default:
return state
}
}
const userReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USER:
case FETCH_ME:
return {
...state,
loading: true
}
case FETCH_USER_SUCCESS:
case FETCH_ME_SUCCESS:
return {
...state,
loading: false,
item: action.data
}
default:
return state
}
}
export default combineReducers({
activeUser: activeReducer,
me: meReducer
})
Your final user state should be something like:
{
me: {
item: null,
loading: false
},
active: {
item: null,
loading: false
}
}