Search code examples
reactjswebsocketstoredispatchuse-reducer

React Store not updating context with React.useReducer


I'm building a simple chat room with React and would like to be able to update the chat window with text by typing in the text field and pressing the send button. I was expecting that when the Store that holds the context invokes React.UseReducer, it would update the context with the text value, that is passed into Dashboard component. When I log the text in the reducer, it shows the value from the text field that should be added to the chat window. However, after running useReducer, the state is still only the initState and doesn't include the sent text.

I thought that allChats would be updated with the text field value in the Store, and then accessed in Dashboard.js by using React.useContext(CTX). allChats logs as only the initState that is in the store.

How can this be updated to pass the text into the context so that the chat window would display it?

Dashboard.js


export default function Dashboard() {

  const classes = useStyles();
  //CTX store
  const {allChats, sendChatAction, user} = React.useContext(CTX);
  const topics = Object.keys(allChats);
  //local state
  const [activeTopic, changeActiveTopic] = React.useState(topics[0])
  const [textValue, changeTextValue] = React.useState('')


  return (
    <div className={classes.root}>
      <Paper className={classes.root}>
        <Typography variant="h4" component="h4">
          Chat Room
        </Typography>
        <Typography variant="h5" component="h5">
          {activeTopic}
        </Typography>
        <div className={classes.flex}>
          <div className={classes.topicsWindow}>
            <List>
              {topics.map((topic) => (
                <ListItem onClick={e=> changeActiveTopic(e.target.innerText)} key={topic} button>
                  <ListItemText primary={topic} />
                </ListItem>
              ))}
            </List>
          </div>
          <div className={classes.chatWindow}>
            { 
              allChats[activeTopic].map((chat, i) => (
              <div className={classes.flex} key={i}>
                <Chip label={chat.from} className={classes.chip} />
                <Typography variant='body1' gutterBottom>{chat.msg}</Typography>
              </div>
            ))
          }
          </div>
        </div>
        <div className={classes.flex}>
        <TextField
         label="Send a Chat"
         className={classes.chatBox}
         value={textValue}
         onChange={e => changeTextValue(e.target.value)}

         />

          <Button
            variant="contained"
            color="primary"
            className = {classes.button}
            onClick ={() => {
              sendChatAction({from: user, msg: textValue, topic: activeTopic});
              changeTextValue('');
            }}
          >
            Send
        </Button>
        </div>
      </Paper>
    </div>
  );
};

Store


import React from 'react'
import io from 'socket.io-client'

export const CTX = React.createContext()


const initState = {
  general: [
    {from:'matt',msg:'yo'},
    {from:'jeff',msg:'yo'},
    {from:'steve',msg:'yo'},
  ],
  topic2:[
    {from:'bill',msg:'yo'},
    {from:'joe',msg:'yo'},
    {from:'dave',msg:'yo'},
  ]
}

function reducer(state, action){
   //payload logs as:
   //{from:'from',msg:'msg',topic:'topic'}
  const {from, msg, topic} = action.payload;
  switch(action.type){
      case 'RECEIVE_MESSAGE':
        return {
          ...state,
          [topic]:[
            ...state[topic],
            {from,msg}
          ]
        }
        default:
          return state
  }
}

let socket;

function sendChatAction(value){
  socket.emit('chat message', value);
}
export default function Store(props){
  const [allChats,dispatch] = React.useReducer(reducer, initState);
  if(!socket){
    socket = io(':3001');
    socket.on('chat message', function(msg){
       //msg logs with text field value that we need in 
       //chatWindow on Dashboard.
      dispatch({type:'RECEIVE_MESSAGE', payload: msg});
    });
  }
      //allChats logs only as initState, above.
const user = 'matt' + Math.random(100).toFixed(2);

return (
  <CTX.Provider value={{allChats,sendChatAction,user}}>
  {props.children}
  </CTX.Provider>
)

}

index.js

var app = require('express')()
var http = require('http').createServer(app);
var io = require('socket.io')(http)

app.get('/',function(req,res){
  res.send('<h1>hello world</h1>')
});

io.on('connection',function(socket){
  console.log('a user connected',socket);
  socket.on('chat message',function(msg){
    console.log('message: ' + JSON.stringify(msg))
    io.emit('chat message', msg)
  });
})

http.listen(3001,function(){
  console.log('listening on *:3001')
});


Solution

  • I put together a working example, stubbing out your socket calls with a simple setTimeout.

    Probably the most interesting part is where I moved the sendChatAction function into the component so it could use the dispatch method of the reducer:

    export default function Store(props) {
      const [allChats, dispatch] = React.useReducer(reducer, initState);
    
      function sendChatAction(value) {
        dispatch({
          type: "RECEIVE_MESSAGE",
          payload: value
        });
      }
    
      ...
    }
    

    Everything appears to work. Compare your code to this:

    Store.jsx

    import React, { useEffect } from "react";
    
    export const CTX = React.createContext();
    
    const initState = {
      general: [
        { from: "matt", msg: "yo" },
        { from: "jeff", msg: "yo" },
        { from: "steve", msg: "yo" }
      ],
      topic2: [
        { from: "bill", msg: "yo" },
        { from: "joe", msg: "yo" },
        { from: "dave", msg: "yo" }
      ]
    };
    
    function reducer(state, action) {
      //payload logs as:
      //{from:'from',msg:'msg',topic:'topic'}
      const { from, msg, topic } = action.payload;
      switch (action.type) {
        case "RECEIVE_MESSAGE":
          return {
            ...state,
            [topic]: [...state[topic], { from, msg }]
          };
        default:
          return state;
      }
    }
    
    export default function Store(props) {
      const [allChats, dispatch] = React.useReducer(reducer, initState);
    
      function sendChatAction(value) {
        dispatch({
          type: "RECEIVE_MESSAGE",
          payload: value
        });
      }
    
      useEffect(() => {
        //msg logs with text field value that we need in
        //chatWindow on Dashboard.
        setTimeout(() => {
          sendChatAction({ from: "matt", msg: "hey", topic: "general" });
        }, 3000);
      }, []);
    
      //allChats logs only as initState, above.
      const user = "matt" + Math.random(100).toFixed(2);
    
      return (
        <CTX.Provider value={{ allChats, sendChatAction, user }}>
          {props.children}
        </CTX.Provider>
      );
    }
    

    Dashboard.jsx

    import {
      Button,
      Chip,
      List,
      ListItem,
      ListItemText,
      Paper,
      TextField,
      Typography
    } from "@material-ui/core";
    import React from "react";
    import { CTX } from "./Store";
    
    export default function Dashboard() {
      const classes = {};
      //CTX store
      const { allChats, sendChatAction, user } = React.useContext(CTX);
      const topics = Object.keys(allChats);
      //local state
      const [activeTopic, changeActiveTopic] = React.useState(topics[0]);
      const [textValue, changeTextValue] = React.useState("");
    
      return (
        <div className={classes.root}>
          <Paper className={classes.root}>
            <Typography variant="h4" component="h4">
              Chat Room
            </Typography>
            <Typography variant="h5" component="h5">
              {activeTopic}
            </Typography>
            <div className={classes.flex}>
              <div className={classes.topicsWindow}>
                <List>
                  {topics.map((topic) => (
                    <ListItem
                      onClick={(e) => changeActiveTopic(e.target.innerText)}
                      key={topic}
                      button
                    >
                      <ListItemText primary={topic} />
                    </ListItem>
                  ))}
                </List>
              </div>
              <div className={classes.chatWindow}>
                {allChats[activeTopic].map((chat, i) => (
                  <div className={classes.flex} key={i}>
                    <Chip label={chat.from} className={classes.chip} />
                    <Typography variant="body1" gutterBottom>
                      {chat.msg}
                    </Typography>
                  </div>
                ))}
              </div>
            </div>
            <div className={classes.flex}>
              <TextField
                label="Send a Chat"
                className={classes.chatBox}
                value={textValue}
                onChange={(e) => changeTextValue(e.target.value)}
              />
    
              <Button
                variant="contained"
                color="primary"
                className={classes.button}
                onClick={() => {
                  sendChatAction({
                    from: user,
                    msg: textValue,
                    topic: activeTopic
                  });
                  changeTextValue("");
                }}
              >
                Send
              </Button>
            </div>
          </Paper>
        </div>
      );
    }
    

    App.js

    import React from "react";
    import Dashboard from "./Dashboard";
    import Store from "./Store";
    import "./styles.css";
    
    export default function App() {
      return (
        <div className="App">
          <h1>Hello CodeSandbox</h1>
          <h2>Start editing to see some magic happen!</h2>
          <Store>
            <Dashboard />
          </Store>
        </div>
      );
    }