I'm relatively new to React and Redux, but I have a basic understanding of how the actions+store+reducers work to enable one way dataflow.
One issue that I've run into as I write my actual app is how to display alerts for errors exactly once per unique error.
The solution that I've come up with works quite well, but I feel like it shouldn't be required, or that with my inexperience I might be missing something.
Essentially it boils down to this:
/* In the reducer: */
const initialState = {
someStateData: 'wow state',
error: undefined,
errorId: 0,
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case SUCCESS:
return {
...state,
someStateData: action.data,
error: undefined,
}
case ERROR:
return {
...state,
error: action.error,
errorId: state.errorId + 1,
}
default:
return state
}
}
/* In the view component: */
class SomeComponent extends Component {
constructor(props) {
super(props)
this.state = {
lastErrorId: 0,
}
}
processHandlers() {
if (this.props.error &&
this.props.lastErrorId !== this.state.lastErrorId) {
this.props.onFailure?.(this.props.error)
this.setState({
...this.state,
lastErrorId: this.props.lastErrorId,
})
}
}
componentDidMount() {
this.processHandlers()
}
componentDidUpdate(prevProps, prevState) {
this.processHandlers()
}
}
const mapStateToProps = state => {
return {
error: state.error,
lastErrorId: state.errorId,
}
}
export default (connect(mapStateToProps)(SomeComponent))
This works great - is there another, better way? More idiomatic with Redux?
Add it to your logic or architecture...once the error occurs, save it, that could be another slice of your store that keeps track errors and other current issues. That is the point of state management, what is happening in your application right now...
Related
From this guide I found here:
https://daveceddia.com/where-fetch-data-redux/
I have a pretty standar reducer that handle data, loading and error:
import {
FETCH_PRODUCTS_BEGIN,
FETCH_PRODUCTS_SUCCESS,
FETCH_PRODUCTS_FAILURE
} from './productActions';
const initialState = {
items: [],
loading: false,
error: null
};
export default function productReducer(state = initialState, action) {
switch(action.type) {
case FETCH_PRODUCTS_BEGIN:
return {
...state,
loading: true,
error: null
};
case FETCH_PRODUCTS_SUCCESS:
return {
...state,
loading: false,
items: action.payload.products
};
case FETCH_PRODUCTS_FAILURE:
return {
...state,
loading: false,
error: action.payload.error,
items: []
};
default:
return state;
}
}
And then the component that call the action and draw based on those states:
import React from "react";
import { connect } from "react-redux";
import { fetchProducts } from "/productActions";
class ProductList extends React.Component {
componentDidMount() {
this.props.dispatch(fetchProducts());
}
render() {
const { error, loading, products } = this.props;
if (error) {
return <div>Error! {error.message}</div>;
}
if (loading) {
return <div>Loading...</div>;
}
return (
<ul>
{products.map(product =>
<li key={product.id}>{product.name}</li>
)}
</ul>
);
}
}
const mapStateToProps = state => ({
products: state.products.items,
loading: state.products.loading,
error: state.products.error
});
export default connect(mapStateToProps)(ProductList);
This works fine the very first time:
products is empty. So the first render will show the empty list. The second time(after the fetch is completed) products will have items.
Now, the problem is what happens if I get outside then component and then re-enter(for example, using react-router).
The very first time It will draw with the cached information in the redux-store. Then after the fetch I will redraw the new list.
Is there any way to avoid this every time?
i have thought a couple of "solutions" but I'm not soure if they will work/are good practices:
setting in the component state a value "fetchId" (example, generating a random UUID) and use it in the fetchProducts action. That value would be saved in the redux store and the compare the redux fetchId with the component. If they are the same, DRAW! If they are differente(the fetchId comes from a different call) I will not draw anything.
Cleaning up redux store calling an action in the componentWillUmount
Store the product I'd in Redux and only render the product if it matches the one the user has selected. If it doesn't match, fetch the requested product.
What does work:
Saga pulls the data from an API. The reducer for UPDATE_LOTS fires up and returns the new state.
Redux store is updated with the correct data as can be observed in the chrome extension and through logging.
What doesn't work:
The componentDidUpdate never fires up. Nor does componentWillReceiveProps when replaced by it.
Since the component never received an update, there's no re-rendering either.
Most of the advice on this topic discusses how people accidentally mutate the state, however in my case I don't do that. I've also tried the following construction {...state, lots: action.data} instead of using ImmutableJS's Map.set with no luck.
Any ideas? Not listing the saga files here because that part of the data flow works perfectly.
The component:
class Lots extends React.Component {
constructor(props) {
super(props);
this.props.onFetchLots();
}
componentDidUpdate() {
console.log('updated', this.props.lots);
}
render() {
const lots = this.props.lots;
console.log('render', lots);
return (lots && lots.length) > 0 ? <Tabs data={lots} /> : <div />;
}
}
Mapping and composition:
function mapDispatchToProps(dispatch) {
return {
onFetchLots: () => {
dispatch(fetchLots());
},
};
}
function mapStateToProps(state) {
return {
lots: state.lots,
};
}
const withConnect = connect(
mapStateToProps,
mapDispatchToProps,
);
const withReducer = injectReducer({ key: 'lots', reducer: lotsReducer });
const withSaga = injectSaga({ key: 'lots', saga });
export default compose(
withReducer,
withSaga,
withConnect,
)(Lots);
Reducer:
export const initialState = fromJS({
lots: false,
});
function lotsReducer(state = initialState, action) {
switch (action.type) {
case FETCH_LOTS:
console.log('fetch lots');
return state;
case UPDATE_LOTS:
console.log('update lots', action.data.data);
return state.set('lots', action.data.data);
default:
return state;
}
}
Everything was correct except for the mapStateToProps function.
Since ImmutableJS was used, I had to access the state property as state.get("lots") instead of state.lots.
Doing so fixed the problem.
I've found lots of similar problems, can't seem to sort my case
I have a component that won't re-render when data changes.
When MODE changes, which is a string, the entity re-renders and updates.
When hotspot.description changes, it won't update.
I can see the description has changed in the store, I can console log the changes all the way to this component.
However I just can't get this component to update when the description changes in hotspot.
Any clues!?
Connected
const mapStateToProps = (state) => {
return {
mode: state.admin.hotspot.mode,
hotspot: state.admin.hotspot.edit,
}
}
Pure
export default class HotspotRenderer extends React.Component {
constructor(props) {
super(props)
this.state = {
hotspot:props.hotspot,
mode:props.mode,
}
}
componentWillReceiveProps(nextProps) {
this.setState({
hotspot : nextProps.hotspot,
mode: nextProps.mode,
})
}
render() {
const {hotspot,mode} = this.state
const isEditingText = hotspot && mode === HOTSPOT_EDIT_MODE.TEXT
const html = hotspot != null ? ReactHtmlParser(draftToHtml(hotspot.description)) : null
return (
<div>
{
isEditingText &&
<Container>
<div className={`hotspot-renderer hotspot${hotspot.id} hotspot-text-default`}><div>{html}</div></div>
</Container>
}
</div>
)
}
}
admin.state.hotspot
const initialState = {
isDraggingNewHotspot: false,
edit:null,
mode:null,
}
export function hotspot(prevState=initialState, action) {
switch(action.type) {
case START_ADD_HOTSPOT: return { ...prevState, isDraggingNewHotspot: true }
case FINISH_ADD_HOTSPOT: return { ...prevState, isDraggingNewHotspot: false }
case ADD_HOTSPOT: return { ...prevState, mode: HOTSPOT_EDIT_MODE.DRAG}
case EDIT_HOTSPOT: return { ...prevState, edit: action.hotspot}
case FINISH_EDIT_HOTSPOT: return { ...prevState, edit: null}
case EDIT_HOTSPOT_MODE: return { ...prevState, mode: action.mode }
case UPDATE_HOTSPOT: return { ...prevState, edit : action.hotspot }
case GO_TO_EDIT_SCENE: return { ...prevState, edit :null,mode :null }
case UPDATE_SCENE_HOTSPOT_SUCCESS: return { ...prevState, edit: processUpdatedHotspot(prevState.edit,action.payload) }
default: return prevState
}
}
function processUpdatedHotspot(prev,update){
if(!prev)
return null
if(!prev.id)
prev.id = update.id
return prev
}
Here is where the description is edited
updateHotspotDescription(description){
let hotspot = this.state.hotspot
hotspot.description = description
hotspot.imageUpdateRequired = true
this.setState({hotspot : hotspot})
this.state.onUpdateHotspot(hotspot)
}
This is dispatched whenever text is changed, via a draft-js editor.
The state is updated with the changes, and another entity is aware of them.
You have to follow the Immutable pattern to update your value, even before passing it to redux (see updating nesting objects in the link).
So before sending hotspot.edit to your reducer be sure to update the nested description object following the immutable pattern like this :
updateHotspotDescription(description){
const hotspot = {
...this.state.hotspot,
description, // shorthand for description: description
imageUpdateRequired: true,
};
this.setState({ hotspot });
this.state.onUpdateHotspot(hotspot);
}
So you have to question yourself, are you sure your action it's being actually taken?
Any non-case in the switch statement will return the previous state, therefore It's normal that It won't re-render.
Some tips to follow to verify if your redux state it's being updated:
Make sure your constants are imported correctly in your actions and in your reducer
Make sure the triggered action it's being properly taken by the reducer
Log the next state in your reducer before your return it, so you can be sure that the next state is the correct one
Follow this steps and let me know if your problem persists
I've always used react-redux connect to configure props but I need to use a react Component to use lifecycle methods. I'm noticing that my props that I'm grabbing from the store seem to be static and they do not update as the store updates.
Code:
class AlertModal extends Component {
title
isOpen
message
componentDidMount() {
const { store } = this.context
const state = store.getState()
console.log('state', state)
console.log('store', store)
this.unsubscribe = store.subscribe(() => this.forceUpdate())
this.title = state.get('alertModal').get('alertModalTitle')
this.isOpen = state.get('alertModal').get('isAlertModalOpen')
this.message = state.get('alertModal').get('alertModalMessage')
this.forceUpdate()
}
componentWillUnmount() {
this.unsubscribe()
}
updateAlertModalMessage(message) {
this.context.store.dispatch(updateAlertModalMessage(message))
}
updateAlertModalTitle(title) {
this.context.store.dispatch(updateAlertModalTitle(title))
}
updateAlertModalIsOpen(isOpen) {
this.context.store.dispatch(updateAlertModalIsOpen(isOpen))
}
render() {
console.log('AlertModal rendered')
console.log('AlertModal open', this.isOpen) <======= stays true when in store it is false
return (
<View
How do I set up title, isOpen, and message so they reflect the store values at all times?
It should be something like this. In your Confirmation component:
const mapStateToProps = (state) => {
return { modalActive: state.confirmation.modalActive };
}
export default connect(mapStateToProps)(Confirmation);
In your reducer index file, is should be something like this:
const rootReducer = combineReducers({
confirmation: ConfirmationReducer
});
I believe you have your own reducer file called ConfirmationReducer here. It should be something like this.
import { ON_CONFIRM } from '../actions';
const INITIAL_STATE = {modalActive: true};
export default function(state = INITIAL_STATE, action) {
console.log(action);
switch (action.type) {
case ON_CONFIRM:
return { ...state, modalActive: action.payload };
}
return state;
}
Make sure you write your own action creator to create an action with the above type and relevant payload of boolean type.
Finally you should be able to access the property from the store inside your Confirmation component like this:
{this.props.modalActive}
You have not posted entire code, so it makes very difficult to give a solution to the exact scenario. Hope this helps. Happy coding!
For me the problem was that I was assigning this.props.myObject to a variable which wasn't deep cloned so I fixed it by using
let prev = Object.assign({}, this.props.searchData)
What I was doing
let prev = this.props.searchData
So I was disturbing the whole page.Seems quiet noob on my part.
this may help you
componentWillReceiveProps(nextProps) {
console.log();
this.setState({searchData : nextProps.searchData})
}
I am creating a React application and integrating Redux to it in order to manage the state and do network requests.
I followed the Todo tutorial and I am following the async example from the redux website, but I am stucked.
Here is my problem, I want, in my application, to fetch a user from a remote server. So the server send me a json array containing an object (maybe it's better if the server send me directly an object ? )
The json I obtain looks like that (I put only two fields but there are more in real) :
[{first_name: "Rick", "last_name": "Grimes"}]
Anyway I can fetch the data from the server but I can't inject user's data into my application, I hope you can help me but the most important is that I understand why it doesn't work.
Here are my several files :
I have two actions, one for the request and the other for the response:
actions/index.js
export const REQUEST_CONNECTED_USER = 'REQUEST_CONNECTED_USER';
export const RECEIVE_CONNECTED_USER = 'RECEIVE_CONNECTED_USER';
function requestConnectedUser(){
return {
type: REQUEST_CONNECTED_USER
}
}
function receiveConnectedUser(user){
return {
type: RECEIVE_CONNECTED_USER,
user:user,
receivedAt: Date.now()
}
}
export function fetchConnectedUser(){
return function(dispatch) {
dispatch(requestConnectedUser());
return fetch(`http://local.particeep.com:9000/fake-user`)
.then(response =>
response.json()
)
.then(json =>
dispatch(receiveConnectedUser(json))
)
}
}
reducer/index.js
import { REQUEST_CONNECTED_USER, RECEIVE_CONNECTED_USER } from '../actions
function connectedUser(state= {
}, action){
switch (action.type){
case REQUEST_CONNECTED_USER:
return Object.assign({}, state, {
isFetching: true
});
case RECEIVE_CONNECTED_USER:
return Object.assign({}, state, {
user: action.user,
isFetching: false
});
default:
return state;
}
}
And I have finally my container element, that I have called Profile.js
import React from 'react';
import { fetchConnectedUser } from '../actions';
import { connect } from 'react-redux';
class Profile extends React.Component{
constructor(props){
super(props);
}
componentDidMount() {
const { dispatch } = this.props;
dispatch(fetchConnectedUser());
}
render(){
const { user, isFetching} = this.props;
console.log("Props log :", this.props);
return (
<DashboardContent>
{isFetching &&
<div>
Is fetching
</div>
}
{!isFetching &&
<div>
Call component here and pass user data as props
</div>
}
</DashboardContent>
)
}
}
function mapStateToProps(state) {
const {isFetching, user: connectedUser } = connectedUser || { isFetching: true, user: []}
return {
isFetching,
user: state.connectedUser
}
}
export default connect(mapStateToProps)(Profile)
In the code above, I always have the Is Fetching paragraph being display, but even when I remove it, I cannot access to user data.
I think I have the beginning of something because when I console.log I can see my actions being dispatched and the data being added to the state, but I think I am missing something in the link communication with this data to the UI Component.
Am I on the good way or do I have a lot of things to refactor ? Any help would be very helpful !
Seeing as you are immediately fetching the data I allow myself to assume that isFetching may be true at beginning. Add an initial state to your reducer
state = { isFetching: true, user: null }
Then assuming you setup the reducer correctly:
function mapStateToProps(state) {
const {isFetching, user } = state.connectedUser
return {
isFetching,
user
}
}
Hope this works, feels clean-ish.