skip to Main Content

I am creating a survey questionnaire form with reusable React components for the page layout with state coming from my Redux store. My state has updated, but the updated state is not rendering properly on the page. Specifically on the review route, my item.value are missing. How can I get values to render?

App.jsx

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import './App.css';
import { useDispatch, useSelector } from 'react-redux';
import { Route, useHistory } from 'react-router-dom';

function App() {
  const history = useHistory();
  const dispatch = useDispatch();
  const feedbackSchema = useSelector((store) => store.feedbackSchema);
  const [inputValue, setInputValue] = useState('');

  useEffect(() => {
    fetchFeedback();
  }, []);

  const fetchFeedback = () => {
    axios({
      method: 'GET',
      url: 'api/feedback',
    })
      .then((response) => {
        dispatch({
          type: 'SET_FEEDBACK',
          payload: response.data,
        });
      })
      .catch((error) => {
        console.log('You had a axios GET error', error);
      });
  };

  const handleSubmit = (item, index) => {
    console.log(item, inputValue);
    dispatch({
      type: 'SET_VALUE',
      payload: [item, inputValue],
    });
    history.push(
      feedbackSchema[index + 1]?.route
        ? `/${feedbackSchema[index + 1]?.route}`
        : '/review'
    );
  };

  return (
    <div className="App">
      <header className="App-header">
        <h1 className="App-title">Feedback!</h1>
        <h4>Don't forget it!</h4>
      </header>
      {feedbackSchema.map((item, index) => {
        return (
          <div key={item.key} className="feedback-container">
            <Route exact path={`/${item.route}`}>
              <h1>{item.header}</h1>
              <form>
                <div className="feedback-input">
                  <p className="feedback-topic">{item.topic}</p>
                  <input
                    type="text"
                    value={inputValue}
                    onChange={(event) => setInputValue(event.target.value)}
                  />
                  <br />
                  <button
                    type="button"
                    onClick={() => handleSubmit(item.key, index)}
                  >
                    Next
                  </button>
                </div>
              </form>
            </Route>
          </div>
        );
      })}
      <Route exact path={`/review`}>
        <h1>Review Your Feedback</h1>
        {feedbackSchema.map((feedback) => {
          return (
            <p key={feedback.key}>
              {JSON.stringify(feedback)}
              {feedback.key}: {feedback.value}
            </p>
          );
        })}
      </Route>
    </div>
  );
}

export default App;

Store.js

import { applyMiddleware, combineReducers, createStore } from "redux";
import logger from "redux-logger";

const feedbackList = (state = [], action) => {
  if (action.type === 'SET_FEEDBACK') {
    return action.payload;
  }
  return state;
}

const feedbackSchema = (
  state = [
    { key: "feeling", route: "", header: "How are you feeling today?", topic: "Feeling?", value: "" },
    { key: "understanding", route: "understanding", header: "How well are you understanding the content?", topic: "Understanding?", value: "" },
    { key: "support", route: "support", header: "How well are you being supported?", topic: "Support?", value: "" },
    { key: "comments", route: "comments", header: "Any comments you want to leave?", topic: "Comments", value: "" }
  ],
  action
) => {
  if (action.type === 'SET_VALUE') {
    state.map((feedback) => {
      if (feedback.key === action.payload[0]) {
        feedback.value = action.payload[1]
      }
      return feedback;
    })
  }
  return state;
}

const store = createStore(
  combineReducers({
    feedbackList,
    feedbackSchema,
  }),
  applyMiddleware(logger)
)

export default store;

index.jsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './components/App/App';
import { Provider } from 'react-redux';
import { HashRouter as Router } from 'react-router-dom';
import store from './store';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <Router>
        <App />
      </Router>
    </Provider>
  </React.StrictMode>
);

UI not rendering state/data

2

Answers


  1. The state is updated but the UI is not updating because react-redux checks if the data is changed by comparing the address of the data.

    In your Store.js file

    const feedbackSchema = (state = [], action) => {
        if (action.type === 'SET_VALUE') {
            state.map((feedback) => {
                if (feedback.key === action.payload[0]) {
                    feedback.value = action.payload[1]
                }
                return feedback;
            })
        }
        return state;
    }
    

    You are returning same data by updating the state. Hence the reducer compares if address of state is equal to address of state. This makes it think no changes are made.

    Instead you can do this,

    const feedbackSchema = (state = [], action) => {
        if (action.type === 'SET_VALUE') {
           const temp = state.map((feedback) => {
                if (feedback.key === action.payload[0]) {
                    feedback.value = action.payload[1]
                }
                return feedback;
            })
        }
        return temp;
    }
    

    By returning a new variable temp reducer will understand the changes are made and UI will be updated.

    Login or Signup to reply.
  2. Issue

    You are mutating state and not returning new state references.

    const feedbackSchema = (state = [....], action) => {
      if (action.type === 'SET_VALUE') {
        state.map((feedback) => {
          if (feedback.key === action.payload[0]) {
            feedback.value = action.payload[1] // <-- mutation!!!
          }
          return feedback;
        })
      }
      return state; // <-- same reference as previous state
    }
    

    React works by using shallow reference equality checks and if the state is the same reference as the previous render cycle React won’t necessarily trigger a component rerender.

    Solution Suggestions

    To address this correctly all state, and nested state, that is being updated necessarily needs to be a new object reference.

    I suggest the following re-write:

    const feedbackSchema = (state = [....], action) => {
      if (action.type === 'SET_VALUE') {
        return state.map((feedback) => { // <-- array.map creates new array reference
          if (feedback.key === action.payload[0]) {
            // Updating this feedback, create new object reference
            return {
              ...feedback,
              value: action.payload[1],
            };
          }
    
          // Not updating feedback, return current feedback reference
          return feedback;
        })
      }
    
      // Just return current state
      return state;
    }
    

    It is more conventional to use switch statements in reducer functions though, so it could also be re-written to a more standard pattern/form:

    const feedbackSchema = (state = [....], action) => {
      const { type, payload } = action;
    
      switch(type) {
        case 'SET_VALUE':
          const [key, value] = payload;
          return state.map((feedback) => feedback.key === key
            ? { ...feedback, value }
            : feedback
          );
    
        default:
          return state;
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search