I'm working on a mini project where a user shall be able to edit book info. The app presents the user with a filter to choose Fiction or Science subject category. Once chosen, he is shown a list of books where he can change the details of the book by clicking the edit button. On editing, he can choose to either save or cancel the form details. This entire workflow is on a single page! I'm using React, Redux, and Redux-form for this purpose where component I- Selection category (Fiction, Science), component II- list of books for that category and component III- form to edit book details. See the attached image to imagine the app.
Working of the app: Once the user chooses Fiction/Science, an action is dispatched to list available books. Once he clicks Edit button for a book, another action is dispatched to display the form pre-filled with the book's values (so that the user can edit it).
The Problem: Unfortunately, my Edit form component is not rendering desired values due to unavailability of props to this component; even though I can see the success of my redux action with correct state/props.
Anatomy of code:
SubjectChoice.jsx
const mapDispatchToProps = dispatch => ({
fetchBooks: favoriteSubject => dispatch(getBooksList(favoriteSubject)),
fetchSubjects: _ => dispatch(getSubjectsList())
});
So, this component correctly dispatches getSubjectsList and I can use fetchSubjects as props on componentDidMount. Similarly, it dispatches another action getBooksList(favoriteSubject) according to the chosen subject and fetchBooks can be used as props on handleChange callback (These actions make API request; in fact, all does)
BookList.jsx
const mapDispatchToProps = dispatch => ({
fetchBookDetails: bookId => dispatch(getBookDetails(bookId))
});
So, this component correctly dispatches getBookDetails action on handleEdit which is Edit button callback.
BookEdit.jsx
const mapStateToProps = state => ({
book: state.book.bookDetails,
...
}...
render() {
const { bookDetails, loading, error } = this.props.book;
...
}
Now, this component keeps on waiting to re-render but it never receives the props. I see the actions, reducers dispatching correctly in the console (from BooksList.jsx component) but never see them reflecting on screen.
How SO answers are not helping me get out of this situation: I saw answers where it's specified that redux does a shallow comparison and if it doesn't find a subtle difference in the previous and next state then it skips rendering the app. However, as mentioned in the third step I see the action and reducer with previous and next state correctly.
If BookEdit isn't connected to the store or the props aren't being passed from a parent component you will not have access to them. Either connect your BookEdit...
import { connect } from 'react-redux;
...
const mapStateToProps = ({ book }) => {
const { bookDetails, loading, error } = book;
return { bookDetails, loading, error };
}
export default connect(mapStateToProps)(BookEdit);
Or pass them in from a parent connected component that is bringing in props from your store.
Every time Redux actions are dispatched, mapStateToProps will be executed. The connect function from react-redux will only re-render your component when the props from mapStateToProps are not equal to the previous props.
Related
I am trying to get my head around a scenario where I am dispatching a synchronous redux action (using createAction of typesafe-actions) and soon after that making a network call that relies on updated props from the store.
Scenario:
Inside clearFilters handler function (handler function invoked on click of clear filters button), I am dispatching a synchronous action and then making a network call as below:
clearFilters = (): void => {
this.props.resetFilters(); //action dispatched
this.refreshData; //network call
};
Inside the refreshData function, my component expects updated filter props and based on it, it creates a searchCondition to be passed to the list api call as payload.
refreshData = (): void => {
const { listData, filters } = this.props; //get the filters prop
//Expected filters to be updated from the store post dispatch of action
const SearchCondition: SearchCondition = createSearchConditions(filters);
listData({
SearchCondition,
MaxResults: this.maxRecordsCount,
SortFields: this.getSortFields(),
}),
);
};
My component is subscribed to the filters prop using mapStateToProps:
const mapStateToProps = (state: RootState) => ({
filters: state.common.filter.filters,
});
Given that is the state of the problem I am facing, I tried to debug what happens by placing debug points in the code:
When the action is dispatched (inside clearFilters function)
Inside the reducer, where updated state is returned.
When the network call is invoked (inside clearFilters function)
In the refreshData call.
After reducer returns updated state, as per the debugging knowledge, store did not send the updated props right away. Rather, the control goes back to the next line i.e. this.refreshData() which make network call with old filters data. Only after the clearFilters function call finishes, in my componentDidUpdate, i can see that props update happen.
Does that signifies redux state change back to the store and eventually subscribed prop updates happen in an ASYNC way? If so, how does it happen? Does store sending the updated props executes in the main thread?
Any pointers/documentation would be really helpful.
The dispatch is synchronous, and the queueing of the React updates is synchronous. However, React will not re-render that component until after this whole event processing is completed, and this.props will not be updated until after that render happens. So, no, you cannot access this.props right after dispatching an action and expect that it has been updated. That will never be true.
I would suggest reading these posts that go into extensive detail on both React and React-Redux:
A (Mostly) Complete Guide to React Rendering Behavior
The History and Implementation of React-Redux
Using Redux, is it true that any component and sub-component on the page get all data of the one and only store, and be able to send out any action at all, even if it is not meant for that component to send out?
Can all the components use
const store = createStore(mainReducer);
let state = store.getState();
and be able to see all states of the whole app? Can any component dispatch any action all all? So for example, if there is Counter component and a Comment component, can the Comment component accidentally send out a "INCREASE_COUNT" action?
Using Redux any component "can" access any data in the store, but for the access you have to 'connect' it with the store. When you 'connect' you also specify a map to which part you want this component to access. That's how you are in control, it only gets access to what you want only.
The same goes for actions. You have to map the actions also - which component can dispatch which action, when your 'connect' to the store.
Check this out for more info - https://redux.js.org/basics/usage-with-react
To most part of your question, it seems the answer is Yes.
Yes, the components can access the whole store ( one it has subscribed to ) and can dispatch actions when needed. I do not think there is any way you can put action/store behind some restrictions.
can the Comment component accidentally send out an "INCREASE_COUNT" action? Yes if you try to dispatch it again from the child component.
If you could add any specific example you have to ask, I can add more to my answer.
I hope it helps you !
" every component has access to the store" is wrong, it is like this " every component has access to the state and actions in the store that you "the developer" specify.
for a component to able to access the store, you need to wrap it in the connection function like so
import { connect } from "react-redux";
// Your component
export default connect(mapStateToProps, dispatchActionToProps);
// the component will only have access to the store props and actions that you specify
// in mapStateToProps and dispatchActionToProps
const mapStateToProps = state => {
return {
// state is the result of combineReducers
// whatevery key the component needs you can specify here
};
}
const dispatchActionToProps = dispatch => {
return {
// your store actions
};
}
I've got a simple login dialog that uses redux (just the dialog is shown below for reference). Each time the user types a character into either of the input fields a state change is fired through redux and when the button is pressed a state change also fires.
I plan on changing the state directly when the button is pressed to be something like "LoginPending", and when the REST call returns, the state will change to either "LoggedIn" or "LoginFailed".
My current plan is to add an if statement to the MapStateToProps method that check the old LoginPending status to see if that changed, and if it did, then dispatch the proper action (In reality I will execute a toast notify message).
My question is, "Is this the proper way to check for something like "logged in""? It seems awkward to do it in MapStateToProps, but since there are so many state changes happening (because of the onchange in the input elements), I can't think of a better place to do it.
class App extends Component {
....
render() {
return (
<div className="App">
<input onChange={this.handleChange('username')}/><br/>
<input onChange={this.handleChange('password')}/><br/><br/>
<button onClick={this.handleClick}>Login</button>
</div>
);
}
}
There are several ways to do it. One of the most popular way is to use redux-thunk. docs and example / official recommendation
Then all the logic will reside on the action creator:
export const exampleAction = (username, password) => (dispatch, getState) => {
dispatch({type: "exampleLoading"}); // the store sets a state variable so the view can render something while this "loading" state is true
doApiCall(username,password).then(response => {
// here the feedback message is added on the store, so my feedback manager can render the new feedback message
dispatch({type: "addFeedback", payload:{message:"success!", type:"success"}});
// here the store cleans up the "loading" variable and uses the api response if needed
dispatch({type: "exampleLoaded", payload: response});
});
}
My questions is a conceptual one and based on the issue outlined in this post: React Redux capture updated store state after updating database. I don't think any code is needed to understand or be able to answer it. But if not it is at the link above.
I think I might have missed a small detail about the react/redux state update process following an action that changes the back-end data that a state variable reflects. My question is: When I dispatch a save action, should I then also be dispatching a request to update any state that depends on that underlying data?
So for example, right now the way I'm thinking about it and implementing my code is as follows:
app starts and ParentComponent loads and dispatches GET_DATA on componentDidMount which initializes state variable data which is reflected on ParentComponent in a table
when a link is clicked on ParentComponent, ParentComponent renders ChildComponent which is a react-modal popup that displays elements of data so it can be updated
there is and Save and Close button on ChildComponent; when you click the button, SAVE_DATA is dispatched and the changes to data that are made on ChildComponent get saved to the database
THIS is where my question arises... at this point should I also be calling GET_DATA to dispatch the process of "refreshing" data in my state? Would this be the right way to handle saving data to a database when using redux so that all components that rely on data get updated?
Note: What I'm currently doing is that after step 3, I am simply triggering a refresh function in ParentComponent so that it rerenders and hence reflects data in state. The epiphany I just had is that there is no way for data in state to reflect the new saved data because GET_DATA has not been dispatched after saving and rerendering the component does not trigger GET_DATA.
Are my assumptions correct? Should I be calling GET_DATA somewhere else in my ParentComponent like ComponentWillReceiveProps? The issue I had here is that maybe I'm doing something wrong, but it triggers an endless loop. Somehow though I feel that is the only place where I can address my need to dispatch GET_DATA after the local ParentComponent state is changed by setting refresh (a ParentComponent state variable) to true.
I think it would benefit you to refactor your actions a bit to take advantage of the action/middleware/reducer pattern.
You would have an action GET_TRANSACTIONS, that would take your year param. Your transactionsMiddleware would respond to the GET_TRANSACTIONS action by making your fetch request and would dispatch GET_TRANSACTIONS_SUCCESS with the respond data on success. You transactions reducer would then process the data into your store.
actions
export const getTransactions = year => {
return {
type: "GET_TRANSACTIONS",
year
};
};
export const getTransactionsSuccess = payload => {
return {
type: "GET_TRANSACTIONS_SUCCESS",
payload
};
};
middleware
function getTransactions(year) {
fetch().then(response => dispatch(actions.getTransactionsSuccess(response.data));
}
reducer
const getTransactionsSuccess = (state, action) => {
return Object.assign({}, state, newStuffFromActionPayload);
}
You would also have an action SAVE_TRANSACTIONS, which would be what your button would dispatch, along with the data to save. Your transactionsMiddleware would respond to the action by dispatching the update request. Your API would return the data from the updated record.
This is where you would have the middleware dispatch a follow-up action. It could be your getTransactions action, but it'd be even better to dispatch an action that your reducer would respond to by merging in the new data to your store.
actions
export const updateTransaction = payload => {
return {
type: "UPDATE_TRANSACTION",
payload
};
};
export const updateTransactionSuccess = payload => {
return {
type: "UPDATE_TRANSACTION_SUCCESS",
payload
};
};
middleware
function updateTransaction(transUpdate) {
fetch().then(response => dispatch(actions.updateTransactionSuccess(response.data))
}
reducer
const updateTransactionSuccess = (state, action) => {
find the record in the state, update it with data from action.payload
return Object.assign({}, state, updatedRecord);
}
If everything is set up correctly, it should trigger an update on your parent when it detects the change in the store. You avoid making two API calls for every save as well.
I'm using React Native and Redux to create an app that helps users find nearby fast-foods. I've got two components : Map and Settings.
Each component is connected (via react-redux) to its respective piece of state. Map can also dispatch an action creator called apiCall :
Map Component
...
connect(
({ map }) => ({ map }),
{ apiCall }
)(Map)
Settings Component
...
connect(
({ settings }) => ({ settings })
)(Settings)
I would like the action creator apiCall to read values from both map and settings pieces of state : this.props.apiCall(map, settings).
However, I want to avoid connecting my Map component to state.settings because it would re-render each time state.settings updates.
I'm still quite confused and have not found "the right approach" in solving this issue. These are the things I tried :
Connecting Map to state.settings and using shouldComponentUpdate() to prevent useless re-renders
Using getState() from the action creator to read state.settings value
Encapsulating everything in another higher component and then passing down specific props
The first two worked but seemed a bit anti-pattern and the third one was still triggering re-renders. Not quite sure why even though it felt like a good solution. I have not tried selectors yet but it seems to be another alternative.
To sum up, my question is :
How to dispatch an action that needs to read values from different pieces of state while avoiding unnecessary re-renders ?
You should use redux-thunk. That way you can return a function (a thunk) in your action creator, which will have access to state
const apiCall = (apiCallArgs) => {
return (dispatch, getState) => {
let mapState = getState('map');
let settingsState = getState('settings');
return dispatch(yourApiAction);
}
}