Sai
Sai

Reputation: 841

With redux, where do I place my logic for updating state?

I am trying to incorporate redux into an existing application I made which gets geoJSON from an api and loads a map and can be filtered.

Before using redux, I created a handle function that filters the state and updates it. This is then passed down to a child component to be used in a button.

I am struggling to figure out where i put this code now that I am learning Redux. Does it go inside my reducer or as an action creator? Can someone please advise?

handle function inside parent class component

filterMaterial = (name) => {
    const filteredData = this.state.geoJSON.features.filter(item => item.properties.material === name ? item : false);

    const newobj = {
        type: "FeatureCollection",
        totalFeatures: filteredData.length,
        features: filteredData
    }

    this.setState({mapJSON: newobj});
}

below is my current code relating to redux, how can i convert the above to work with redux?

actions

import { DATA_LOADED } from "../constants/action-types";

export function getData() {
    return function(dispatch) {
        const request = async () => {
            const url = `http://localhost:4000/api/assets`;

            try {
                const response = await fetch(url);
                const data = await response.json();
                
                dispatch({ type: DATA_LOADED, payload: data });
            } catch(err) {
                console.log(err)
            }
        }

        return request();
    };
}

reducers

import { DATA_LOADED } from "../constants/action-types";

const initialState = {
    geoJSON: {},
    mapJSON: {}
};

function rootReducer(state = initialState, action) {
    if(action.type === DATA_LOADED) {
        return Object.assign({}, state, {
            geoJSON: state.geoJSON = {...action.payload},
            mapJSON: state.mapJSON = {...action.payload}
        });
    }

    return state;
};

export default rootReducer;

store

import { createStore, applyMiddleware, compose } from "redux";
import rootReducer from "../reducers/index";
import thunk from "redux-thunk";

const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
    rootReducer,
    storeEnhancers(applyMiddleware(thunk))
);

export default store;

parent component

//...code

const mapStateToProps = state => {
    return { 
        geoJSON: state.geoJSON,
        mapJSON: state.mapJSON
    };
};

export default connect(mapStateToProps, {getData})(FetchData);

Upvotes: 1

Views: 517

Answers (2)

Drew Reese
Drew Reese

Reputation: 202666

What you have looks good other than some state object mutation in your reducer.

import { DATA_LOADED } from "../constants/action-types";

const initialState = {
    geoJSON: {},
    mapJSON: {}
};

function rootReducer(state = initialState, action) {
    if(action.type === DATA_LOADED) {
        return Object.assign({}, state, {
            geoJSON: state.geoJSON = {...action.payload}, // this will mutate current state!
            mapJSON: state.mapJSON = {...action.payload}
        });
    }

    return state;
};

export default rootReducer;

The pattern is to simply create your new state objects, spreading in current state for what you want to perpetuate.

import { DATA_LOADED } from "../constants/action-types";

const initialState = {
    geoJSON: {},
    mapJSON: {}
};

function rootReducer(state = initialState, action) {
    if(action.type === DATA_LOADED) {
        const { geoJSON, mapJSON } = action.payload;
        return {
            ...state,
            geoJSON,
            mapJSON
        });
    }

    return state;
};

export default rootReducer;

This OFC assumes that each time your state is updated you simply replace the existing geoJSON and mapJSON values. The following would be the pattern if you need to merge in new payloads.

return {
  ...state,
  geoJSON: { ...state.geoJSON, ...geoJSON },
  mapJSON: { ...state.mapJSON, ...mapJSON },
});

The important thing to remember with reducers is that they should be pure functions with zero side-effects, and they should always return new object references so shallow reference equality checks work in react for DOM diffing.

As for the filtering. When filtering you typically leave your source of truth intact and simply filter the "view" of the data. In this case you definitely want to do the filtering within the component. It will get the geo data mapped in from the connect HOC as a prop so you can simply filter right from the props object.

const filteredData = this.props.geoJSON.features.filter(item => item.properties.material === name ? item : false);

const newobj = {
    type: "FeatureCollection",
    totalFeatures: filteredData.length,
    features: filteredData
}

This can be done within the render function. There shouldn't be much need to duplicate data within the local component state.

Additional Questions from Comments

If i am filtering the state through props as you suggested, how can I update the state? Do you just call a dispatch?

Yes, updating redux state is done via dispatched actions and reducers. Perhaps we're not quite on the same page about what "filter" means though. Typically when you filter data you are merely filtering the source into a reduced dataset for display. This doesn't require mutating or updating the source. Contrast this with something like adding or deleting entries in the source data (Note: in implementation a filter function maybe used to remove specific data) where an action must be dispatched to act upon the state, i.e. user clicked a delete button.

If you need to persist filtered data then I'd suggest leaving the source data intact, and instead store the filter criteria in state. You can then use both slices of state to derive what is displayed.

Also if I wrap it in a function can I pass it down to child components?

Sure, you can pass just about anything as a prop to children components. The beauty of redux, react context API, and Higher Order Components, is that they all help solve the problem of prop drilling. Any component can subscribe to the redux store and extract just the data they need, and access a dispatch function, or even have their action creators automagically wrapped by a call to dispatch. So while you can create functions that use dispatch and pass them on to children this isn't really the recommended use-case for redux in react.

Upvotes: 2

Pavlos Karalis
Pavlos Karalis

Reputation: 2966

Are you permanently changing store state or just rendering a filtered component state? If you're just rendering, you keep it in your component. If your intention is to permanently update store state, you still keep it in the component, but dispatch the return (newobj) as a payload in an action creator:

Example action creator file:

const ACTION_TYPE_NAME = "ACTION_TYPE_NAME";
export default function actionCreatorName(payloadName) {
    return { type: ACTION_TYPE, payloadName }
}

Example reducer:

//set default state here
const initial = {mapJSON: "", geoJSON: ""}

export default function rootReducer(state = initial, action) {
    switch (action.type) {
        case ACTION_TYPE_NAME:
            //return new state object with updated prop
            return {...state, mapJSON: action.payloadName}
        default: 
            return state
    }
}

Example Component connection/action dispatch:

import React from 'react'
import { connect } from 'react-redux'
import actionCreatorName from '../actions/actionCreatorName.js'


//any state you want to import from store
const mapStateToProps = state => {
    return {
      mapJSON: state.mapJSON,
      geoJSON: state.geoJSON
    }
}
//any imported action creators you want to bind with dispatch
const mapDispatchToProps = {
   actionCreatorName
}

//set the above as props
let ComponentName = ({actionCreatorName, mapJSON, geoJSON}) => {
    const yourFunction = () => {
       //do stuff then dispatch
       actionCreatorName(newobj)
    }
    return (
       <div onClick={yourFunction}></div>
    )
}

ComponentName = connect(
    mapStateToProps,
    mapDispatchToProps
)(ComponentName) 

export default ComponentName

Upvotes: 1

Related Questions