{"componentChunkName":"component---src-templates-post-js","path":"/react-hooks-redux-toolkit-yet-another-chat-app","result":{"data":{"mdx":{"frontmatter":{"title":"React Hooks, Redux Toolkit - Yet Another Chat App","date":"09/26/2020"},"body":"function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"React Hooks, Redux Toolkit - Yet Another Chat App\",\n  \"slug\": \"react-hooks-redux-toolkit-yet-another-chat-app\",\n  \"date\": \"2020-09-26T00:00:00.000Z\",\n  \"image\": \"./images/chat.jpg\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, [\"components\"]);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"Action Cable is the Rails way to integrate WebSockets, allowing for bi-directional communication with minimal overhead between client and server. I recently made a group chat app with React hooks, Redux Toolkit and Action Cable to support real-time features while leveraging a Rails API backend to preserve data. Below is a demo of these features, including:\"), mdx(\"p\", null, \"\\uD83D\\uDE80 Real-time message updates among users in the same chat room\"), mdx(\"p\", null, \"\\uD83D\\uDE80 Unread message prompts from inactive rooms\"), mdx(\"p\", null, \"\\uD83D\\uDE80 Displaying the number of unread messages after the user closes browser and logs back in\"), mdx(\"p\", null, \"While building this app, I found resources to be scarce on this subject. The Rails guide lacks clarity for using Rails as API. Tutorials from popular articles do not take advantage of Redux for more integrated features. This post is my attempt to expand the knowledge base. It incorporates React hooks and \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://redux-toolkit.js.org/\"\n  }), \"Redux Toolkit\"), \", \\u201Cthe official, opinionated, batteries-included toolset for efficient Redux development.\\u201D\"), mdx(\"p\", null, \"This post is organized in three sections: configuration, send messages (from client) and receive messages (by client). As we encounter Redux Toolkit code snippets in the post, I provide excerpts from its documentation for background as the Toolkit might still be relatively new, although most of the concepts shouldn\\u2019t.\"), mdx(\"p\", null, \"If there\\u2019s interest, I will add a bonus section at the end to describe how to preserve the \\u201Clast read at\\u201D data in the backend for displaying number of unread messages after user leaves and logs back in.\"), mdx(\"h3\", null, \"Configuration\"), mdx(\"h4\", null, \"Rails Action Cable setup\"), mdx(\"p\", null, \"\\u2757\\uFE0F This section assumes you already have a working Rails API set up.\"), mdx(\"ol\", null, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Include the redis gem for WebSocket communication; uncomment it in your gemfile and \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"bundle\"), \":\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-bash\"\n  }), \"    # Use Redis adapter to run Action Cable in production 4.0\\n    gem 'redis'\\n\")), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"The default Publish-Subscribe (pubsub) queue for Action Cable is \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"redis\"), \" in production and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"async\"), \" in development and test environments. It was easier for me to work with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"redis\"), \" even in development.\"), mdx(\"ol\", {\n    \"start\": 2\n  }, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Change the adapter setting in your \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"config/cable.yml\"), \" to the following:\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-bash\"\n  }), \"    # config/cable.yml\\n\\n    development:\\n      adapter: redis\\n\")), mdx(\"p\", null, \"You may need to install redis locally through \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"brew install redis\"), \" and launch through \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"brew services start redis\"), \".\"), mdx(\"ol\", {\n    \"start\": 3\n  }, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Mount the \\u2018/cable\\u2019 endpoint in your routes to receive connection requests from client.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-bash\"\n  }), \"    # config/routes.rb\\n\\n    Rails.application.routes.draw do\\n      mount ActionCable.server => '/cable'\\n    end\\n\")), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"Client side connection request will look like this: \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"CableApp.cable = actionCable.createConsumer('ws://localhost:3000/cable')\")), mdx(\"h4\", null, \"React Create App with Redux Toolkit and Context for Action Cable connection\"), mdx(\"ol\", null, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Create a react app with Redux Toolkit; you can delete any unnecessary starter code, such as the counter example they include in the Toolkit.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-bash\"\n  }), \"    npx create-react-app chat-app-client --template redux\\n\")), mdx(\"ol\", {\n    \"start\": 2\n  }, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Install the \", mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://www.npmjs.com/package/actioncable\"\n  }), \"actioncable\"), \" package; it does not have any extra dependencies.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-bash\"\n  }), \"    npm install --save actioncable\\n\")), mdx(\"ol\", {\n    \"start\": 3\n  }, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Establish one Action Cable connection in your root source file and provide this connection to the App component through Context.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"// src.index.js\\n\\nimport React, { createContext } from \\\"react\\\"\\nimport ReactDOM from \\\"react-dom\\\"\\nimport { Provider } from \\\"react-redux\\\"\\n\\nimport actionCable from \\\"actioncable\\\"\\n\\nimport App from \\\"./App\\\"\\nimport store from \\\"./app/store\\\"\\n\\nconst CableApp = {}\\n// change to whatever port your server uses\\nCableApp.cable = actionCable.createConsumer(\\\"ws://localhost:3000/cable\\\")\\nexport const ActionCableContext = createContext()\\n\\nReactDOM.render(\\n  <Provider store={store}>\\n    // omitted any other providers we may have\\n    <ActionCableContext.Provider value={CableApp.cable}>\\n      <App />\\n    </ActionCableContext.Provider>\\n  </Provider>,\\n  document.getElementById(\\\"root\\\")\\n)\\n\")), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"Notice the url no longer starts with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"http\"), \" but \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ws\"), \" , denoting WebSocket requests.\"), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"Notice the term of \\u201CConsumer\\u201D: once you understand Action Cable\\u2019s terminology, the logic should be clear. Refer to the \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://guides.rubyonrails.org/action_cable_overview.html\"\n  }), \"Rails guide\"), \" Section 2 for the architectural stack explanation. Below is a much simplified graphical representation that I attempted based on this sample project. Lines are bi-directional.\"), mdx(\"p\", null, mdx(\"img\", _extends({\n    parentName: \"p\"\n  }, {\n    \"src\": \"https://cdn-images-1.medium.com/max/4320/1*Ju1nRjfFHMJ4WBOgFTy57A.png\",\n    \"alt\": \"Action Cable Graphical Presentation\"\n  })), mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Action Cable Graphical Presentation\")), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"we create the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ActionCableContext\"), \" here and use it to provide \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"CableApp.cable\"), \" to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"App\"), \"\\u2019s component tree. This will allow us to generate subscriptions, through one single connection, to multiple cable channels in the Action Cable server. This can happen across components without us having to pass props down manually at every level of the component tree.\"), mdx(\"p\", null, \"This concludes the basic configuration.\"), mdx(\"p\", null, \"Before we go into details on sending messages and receiving messages, we should first generate our models and associations. This is simply to establish a base understanding of the data relationships for easier reference later when we focus on the fun stuff.\"), mdx(\"p\", null, \"\\u2757\\uFE0F Each chatroom is denoted as a Team in my example.\"), mdx(\"p\", null, \"\\u2757\\uFE0F The membership table is only necessary for preserving data about unread messages; message can serve as a join table if preserving data is not needed.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-bash\"\n  }), \"    rails g resource user name picture_url\\n    rails g resource team name description\\n    rails g resource membership last_read_at:datetime user:references team:references\\n    rails g resource message content user:references team:references\\n\")), mdx(\"h3\", null, \"Send Messages from Client\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Create channel in Rails and prepare for receiving data through WebSockets\")), mdx(\"ol\", null, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Generate the messages channel with Rails command line, which creates the \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"app/channels/messages_channel.rb\"), \" file for us.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-bash\"\n  }), \"    rails g channel messages\\n\")), mdx(\"ol\", {\n    \"start\": 2\n  }, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Set up the generated channel with subscribed, receive and unsubscribed actions, corresponding to the subscribe, send and unsubscribe functions on the client side.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-ruby\"\n  }), \"class MessagesChannel < ApplicationCable::Channel\\n  def subscribed\\n    stop_all_streams\\n    @team = Team.find(params[:id])\\n    stream_for @team\\n  end\\n\\n  def receive(data)\\n    ...\\n  end\\n\\n  def unsubscribed\\n    stop_all_streams\\n  end\\nend\\n\")), mdx(\"p\", null, \"\\u2757\\uFE0F The receive action will be modified in the next section so after the server receives data from font end, it saves the message into database and broadcasts it back to frontend.\"), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"A channel is similar to a controller in a regular MVC setup. It can handle multiple subscriptions with different identifiers.\"), mdx(\"p\", null, \"\\uD83D\\uDCA1\", mdx(\"em\", {\n    parentName: \"p\"\n  }, \" Aside:\"), \" The difference between \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stream_from\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stream_for\"), \" is that the former expects a string while the latter an object. For example: \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stream_from\"), \" \\\"\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"chat_#{params[:room]}\"), \"\\\" vs. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stream_for current_user\"), \". I find it easier to understand \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stream_for\"), \" as it makes the channel more comparable to a controller.\"), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"WebSockets provide two-way communication. I find many articles to use Action Cable to broadcast data to the client while receiving data through HTTP. In my opinion, it is more efficient to also receive data through WebSockets.\"), mdx(\"h4\", null, \"React MessagesList component for sending messages\"), mdx(\"ol\", null, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Create a MessagesList component to render the list of messages for the selected team and the input editor. Prepare it with the Action Cable connection provided by the \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"ActionCableContext.Provider\"), \" from the root source file. And set a local \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"channel\"), \" state, so we can use the subscription created in the \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"useEffect\"), \" hook outside the callback.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"const MessagesList = () => {\\n  const cable = useContext(ActionCableContext)\\n  const [channel, setChannel] = useState(null)\\n\\n  ...\\n\\n  return (\\n    <div>\\n      {renderedMessages}\\n      <Editor sendMessage={sendMessage} />\\n    </div>\\n  )\\n}\\n\")), mdx(\"p\", null, \"\\uD83D\\uDCA1\", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"Remember, the value provided by the context is \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"actionCable.createConsumer('ws://localhost:3000/cable')\"), \".\"), mdx(\"ol\", {\n    \"start\": 2\n  }, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Establish subscription to the \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"MessagesChannel\"), \" in a \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"useEffect\"), \" callback. The \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"MessagesChannel\"), \" is already created in our Rails backend. As described in the Action Cable Graphical Presentation, we can create multiple channels on an Action Cable connection; each channel can receive multiple subscriptions from client side.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"const MessagesList = () => {\\n\\n  ...\\n\\n  useEffect(() => {\\n    const channel = cable.subscriptions.create({\\n      channel: 'MessagesChannel',\\n      id: teamId,\\n    })\\n\\n    setChannel(channel)\\n\\n    return () => {\\n      channel.unsubscribe()\\n    }\\n  }, [teamId])\\n\\n  ...\\n\\n}\\n\")), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"As this example generally follows RESTful conventions, each MessagesList is rendered with the url \\u2018teams/:teamId\\u2019. So it is relatively easy to get the teamId through a \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"useParams\"), \" hook:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"const { teamId } = useParams()\\n\")), mdx(\"p\", null, \"Here we first create the subscription, passing in the channel\\u2019s name and the identifier. This corresponds to the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"subscribed\"), \" action in our backend. It expects an identifier (team id) to find the correct team to stream for.\"), mdx(\"p\", null, \"Then we set our component \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"channel\"), \" state to be the subscription created in the callback. This way, we can use the subscription\\u2019s \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"send\"), \" method (provided by Action Cable by default) outside the callback.\"), mdx(\"p\", null, \"Finally, we clean up by unsubscribing from the channel. This can be done by calling the (surprise!) \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"unsubscribe\"), \" method provided by Action Cable.\"), mdx(\"ol\", {\n    \"start\": 3\n  }, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Create a \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"sendMessage\"), \" function, which gets the message content from the Editor child component and uses the subscription\\u2019s \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"send\"), \" method to send data to our backend. The corresponding action in our backend \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"MessagesChannel\"), \" is \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"receive(data)\"), \".\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"\\nconst MessagesList = () => {\\n\\n  ...\\n\\n  const sendMessage = (content) => {\\n    const data = { teamId, userId, content }\\n    channel.send(data)\\n  }\\n\\n  ...\\n\\n}\\n\")), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside:\"), \" Remember, this function is passed down to the Editor child component and that\\u2019s where it gets the content. Also, \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"channel\"), \" in here refers to the component channel state, which got its value in the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"useEffect\"), \" callback. The \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"receive(data)\"), \" action in our backend expects team id and user id in addition to the message\\u2019s content.\"), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"As mentioned above, we can get the team id pretty easily from the url through a \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"useParams\"), \" hook, but you might wonder where to get the current user id. Redux to the rescue! When user logs in, dispatch an action to add user id to the Redux store. This way, we can use a \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"useSelector\"), \" hook to get current user\\u2019s id:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"const currentUserId = useSelector(state => state.users.currentUser)\\n\")), mdx(\"h3\", null, \"Receive Messages in Client\"), mdx(\"h4\", null, \"Action Cable broadcast data\"), mdx(\"p\", null, \"The backend work for this part is extremely easy as we have already set up our \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"MessagesChannel\"), \" and corresponding actions. We only need to build out our \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"receive(data)\"), \" action so it saves the received message in our database and broadcasts it back to client.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-ruby\"\n  }), \"# app/channels/messages_channel.rb\\n\\nclass MessagesChannel < ApplicationCable::Channel\\n  ...\\n  def receive(data)\\n    user = User.find_by(id: data['userId'])\\n    message = @team.messages.create(content: data['content'], user: user)\\n    MessagesChannel.broadcast_to(@team, MessageSerializer.new(message).serialized_json)\\n  end\\n  ...\\nend\\n\\n# app/serializers/message_serializer.rb\\n\\nclass MessageSerializer\\n  include FastJsonapi::ObjectSerializer\\n  attributes :content, :created_at\\n  belongs_to :team\\n  belongs_to :user\\nend\\n\")), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"Remember we have already found the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"@team\"), \" in our \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"subscribed\"), \" action.\"), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"Notice we no longer need to use params to denote the data received from frontend \\u2014 all information is directly available in a hash as how we shaped it. Remember, we send message like this from our frontend:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"const data = { teamId, userId, content }\\nchannel.send(data)\\n\")), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"The serializer is provided by Fast JSON API. I use it for alleged speed.\"), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"You can also perform the broadcast later with an Active Job job\\u2026 so the data transmission is queued up for better performance. It can be done like this:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-ruby\"\n  }), \"\\n# app/channels/messages_channel.rb\\n\\nclass MessagesChannel < ApplicationCable::Channel\\n  ...\\n  def receive(data)\\n    ...\\n    MessageRelayJob.perform_later(message)\\n  end\\n  ...\\nend\\n\\n# app/jobs/message_relay_job.rb\\n\\nclass MessageRelayJob < ApplicationJob\\n  queue_as :default\\n\\n  def perform(message)\\n    team = message.team\\n    MessagesChannel.broadcast_to(team, MessageSerializer.new(message).serialized_json)\\n  end\\nend\\n\")), mdx(\"h4\", null, \"React frontend receiving messages\"), mdx(\"p\", null, \"Before we get into the steps, we need to clarify where in the React front end we want to receive messages. The MessagesList component described in the above section renders the user\\u2019s active team. As one can only \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"send\"), \" messages in an active team, this component alone is perfectly capable for sending data to our backend. However, we want the user to see unread message prompts from inactive teams \\u2014 the teams displayed in the user\\u2019s teams list but not selected in the url. So the task of receiving broadcasted messages needs to be handled by a TeamListItem component.\"), mdx(\"ol\", null, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Create a TeamListItem component similar to MessagesList.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"... // omitted imports\\n\\nimport { messageReceived } from '../messages/messagesSlice'\\n\\nconst TeamListItem = ({ team }) => {\\n  const dispatch = useDispatch()\\n  const cable = useContext(ActionCableContext)\\n\\n  ...\\n\\n  useEffect(() => {\\n    cable.subscriptions.create(\\n      { channel: 'MessagesChannel', id: team.id },\\n      {\\n        received: (data) => {\\n          dispatch(messageReceived(data))\\n        },\\n      }\\n    )\\n  }, [team, dispatch])\\n\\n  ...\\n\\n  return (\\n    ... // simplified presentation\\n    <Link to={`/teams/${team.id}`}>\\n      {team.name}\\n    </Link>\\n  )\\n}\\n\\nexport default TeamListItem\\n\")), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside:\"), \" This component is used in the TeamsList component like so:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"const renderedTeamListItems = teams.map(team => (\\n  <TeamListItem key={team.id} team={team} />\\n))\\n\")), mdx(\"p\", null, \"Like before, we get the Action Cable connection from the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ActionCableContext\"), \", and establish subscriptions to the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"MessagesChannel\"), \" for each of the teams on the user\\u2019s teams list.\"), mdx(\"p\", null, \"We don\\u2019t need to set a component \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"channel\"), \" state here because there\\u2019s no need to use the subscription outside the callback. It just receives data and dispatches an action to update our Redux store. So what the heck is \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"messageReceived\"), \"? \\u2026\"), mdx(\"ol\", {\n    \"start\": 2\n  }, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Now onto our Redux messagesSlice to receive message and update store.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"// src/features/messages/messagesSlice.js\\n\\n... // omitted imports\\n\\nconst messagesAdapter = createEntityAdapter()\\n\\nconst initialState = messagesAdapter.getInitialState({\\n  ...\\n})\\n\\n... // omitted thunks\\n\\nconst messagesSlice = createSlice({\\n  name: 'messages',\\n  initialState,\\n  reducers: {\\n    messageReceived(state, action) {\\n      const data = action.payload.data\\n      const message = {\\n        id: data.id,\\n        ...data.attributes,\\n        teamId: data.relationships.team.data.id,\\n        userId: data.relationships.user.data.id,\\n      }\\n      messagesAdapter.addOne(state, message)\\n    },\\n  },\\n  extraReducers: {\\n\\n   ...\\n\\n  },\\n})\\n\\nexport const { messageReceived } = messagesSlice.actions\\n\\nexport default messagesSlice.reducer\\n\")), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"If you are not familiar with Redux Toolkit, here\\u2019s an explanation of \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"createSlice\"), \": \\u201Cit takes care of generating action type strings, action creator functions, and action objects. All you have to do is define a name for this slice, write an object that has some reducer functions in it, and it generates the corresponding action code automatically. The string from the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"name\"), \" option is used as the first part of each action type, and the key name of each reducer function is used as the second part.\\u201D So, in our example, the \\\"\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"messages\"), \"\\\" name + the \\\"\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"messageReceived\"), \"\\\" reducer function generated an action type of \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"{type: \\\"messages/messageReceived\\\"}\"), \". \\u201C(After all, why write this by hand if the computer can do it for us!)\\u201D\"), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside:\"), \" Here\\u2019s an explanation of \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"createEntityAdapter\"), \": \\u201Cit provides a standardized way to store your data in a slice by taking a collection of items and putting them into the shape of \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"{ ids: [], entities: {} }\"), \". Along with this predefined state shape, it generates a set of reducer functions and selectors that know how to work with that data.\\u201D Refer to \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://redux.js.org/tutorials/essentials/part-6-performance-normalization\"\n  }), \"Redux Essentials Performance and Normalizing Data\"), \" for more information.\"), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"The entity adapter, in our case messagesAdapter, provides a set of generated reducer functions for adding, updating, and removing entity instances from the entity state. In our \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"messageReceived\"), \" reducer, we\\u2019re using the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"addOne\"), \" reducer function which accepts a single entity and adds it.\"), mdx(\"h4\", null, \"React frontend displaying messages\"), mdx(\"p\", null, \"Now with the newly received message properly stored in our Redux store, we need to display it to our user. If the message belongs to an \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"active\"), \" team, then we should render it in the MessagesList component. Remember, this component is rendered with the \\u2018teams/:iteamId\\u2019 url and displays the message log for the team as well as an input editor. If the message belongs to an \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"inactive \"), \"team, then we need to bold that team\\u2019s name and update the number of unread messages in a badge on our teams list.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Render new messages in the active team\")), mdx(\"ol\", null, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Create selector in the messagesSlice to select messages by team.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"// src/features/messages/messagesSlice.js\\n\\n...\\n\\nexport const { selectAll: selectAllMessages } = messagesAdapter.getSelectors(\\n  (state) => state.messages\\n)\\n\\nexport const selectMessagesByTeam = createSelector(\\n  [selectAllMessages, (state, teamId) => teamId],\\n  (messages, teamId) => messages.filter((message) => message.teamId === teamId)\\n)\\n\")), mdx(\"p\", null, \"Again, if you\\u2019re not familiar with Redux Toolkit, here\\u2019s an explanation of \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"getSelectors\"), \": \\u201CThe adapter object also has a \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"getSelectors\"), \" function. You can pass in a selector that returns this particular slice of state from the Redux root state, and it will generate selectors like \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"selectAll\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"selectById\"), \".\\u201D\"), mdx(\"p\", null, \"The \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"createSelector\"), \" function is re-exported from the \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://github.com/reduxjs/reselect\"\n  }), \"Reselect library\"), \", \\u201Cit takes \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"one or more \"), \"\\u2018input selector\\u2019 functions as argument, plus an \\u2018output selector\\u2019 function.\\u201D When we call \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"selectMessagesByTeam(state, teamId)\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"\\u201CcreateSelector\"), \" will pass all of the arguments into each of our input selectors. Whatever those input selectors return becomes the arguments for the output selector.\\u201D\"), mdx(\"p\", null, \"In our case, we have two input selector functions:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"    selectAlMessages // from getSelectors\\n    (state, teamId) => teamId\\n\")), mdx(\"p\", null, \"Because we pass in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"(state, teamId)\"), \" to the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"selectMessagesByTeam\"), \" selector, \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"createSelector\"), \" will pass them into both of the input selector functions. The result of the first input selector function is \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"messages\"), \" and the second is \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"teamId\"), \". Together they become the arguments of the output selector, which is used to filter the messages.\"), mdx(\"ol\", {\n    \"start\": 2\n  }, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Automagically render the selected messages by team in our MessagesList and update as new messages are added to our store.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"... // omitted other imports\\nimport { selectMessagesByTeam } from './messagesSlice'\\nimport { useSelector } from 'react-redux'\\nimport MessageItem from './MessageItem'\\n\\nconst MessagesList = () => {\\n\\n  ...\\n\\n  const messages = useSelector((state) => selectMessagesByTeam(state, teamId))\\n\\n  const renderedMessages =\\n    messages &&\\n    messages.map((message) => (\\n      <MessageItem key={message.id} message={message} />\\n    ))\\n\\n  ...\\n\\n}\\n\")), mdx(\"p\", null, \"Once a component is hooked up to our store, most of the logic for our data will live in the Redux slices. The result is that we have a clean component which automagically re-renders when the selected state changes.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Render unread message prompts on the teams list\")), mdx(\"ol\", null, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Create selector to select unread messages by team.\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"// src/features/messages/messagesSlice.js\\n\\n... // omitted other imports\\nimport { selectTeamById } from '../teams/teamsSlice'\\nimport { isAfter, parseISO, subYears } from 'date-fns'\\n\\nexport const selectUnreadMessages = createSelector(\\n  [selectMessagesByTeam, selectTeamById],\\n  (messages, team) => {\\n    const lastReadAt = parseISO(team.lastReadAt) || subYears(Date.now(), 1)\\n    return messages.filter((message) =>\\n      isAfter(parseISO(message.created_at), lastReadAt)\\n    )\\n  }\\n)\\n\")), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \\u201C\"), \"Selectors are composable. They can be used as input to other selectors.\\u201D \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"createSelector\"), \" generates memoized selectors. \\u201CMemoized selectors will only recalculate the results if the input selectors return new values\\u201D. It is one of the tools we can use to optimize performance. So I chose to use it to get the unread messages, instead of putting the filtering logic in the component.\"), mdx(\"p\", null, \"As we call \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"selectUnreadMessages(state, teamId)\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"createSelector\"), \" passes both arguments into the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"selectMessagesByTeam\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"selectTeamById\"), \" input selectors. The returned value of the input selectors are \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"messages\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"team\"), \", which becomes the arguments for the output selector.\"), mdx(\"p\", null, \"The team entity includes a \\u201Clast_read_at\\u201D attribute, providing information about when the user looked at the team most recently. It can be \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"null\"), \" if the user joined the team but has not looked at it. So we can filter out the unread messages by comparing the message\\u2019s \\u201Ccreated_at\\u201D attribute with the team\\u2019s \\u201Clast_read_at\\u201D attribute. In the output selector, we use several helper functions from \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://date-fns.org/\"\n  }), \"date-fns\"), \" to help us with the comparison, including \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://date-fns.org/v2.16.1/docs/parseISO\"\n  }), \"parseISO\"), \", \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://date-fns.org/v2.16.1/docs/subYears\"\n  }), \"subYears\"), \", and \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://date-fns.org/v2.16.1/docs/isAfter\"\n  }), \"isAfter\"), \".\"), mdx(\"ol\", {\n    \"start\": 2\n  }, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Automagically bold team name and render number of unread messages on the teams list based on unread messages data in Redux store\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"... // omitted other imports\\n\\nimport { selectUnreadMessages } from '../messages/messagesSlice'\\n\\nconst TeamListItem = ({ team }) => {\\n\\n  ...\\n\\n  const numOfUnreads = useSelector((state) =>\\n    selectUnreadMessages(state, team.id)\\n  ).length\\n\\n  const getFontWeight = () => {\\n    if (location.pathname.slice(7) === team.id) {\\n      return 'fontWeightRegular'\\n    }\\n    return numOfUnreads === 0 ? 'fontWeightRegular' : 'fontWeightBold'\\n  }\\n\\n  const renderedNumOfUnreads = () => {\\n    if (location.pathname.slice(7) === team.id) {\\n      return 0\\n    }\\n    return numOfUnreads\\n  }\\n\\n  return (\\n    // material UI\\n    <Link to={`/teams/${team.id}`}>\\n      <Badge badgeContent={renderedNumOfUnreads()}>\\n        <Typography>\\n            <Box fontWeight={getFontWeight()}>{team.name}</Box>\\n          </Typography>\\n      </Badge>\\n    </Link>\\n  )\\n}\\n\")), mdx(\"p\", null, \"\\uD83D\\uDCA1 \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Aside: \"), \"We create helper functions to prevent the unread prompt effects on the active teams. And we know which team is active based on the url, because we follow the RESTful conventions!\"), mdx(\"h3\", null, \"Conclusion\"), mdx(\"p\", null, \"It took me a long time to experiment with the different ways to use Action Cable with React hooks as most tutorials are written using class components. I hope this post is helpful to those who are figuring out how to integrate Action Cable to their React apps using hooks and Redux!\"));\n}\n;\nMDXContent.isMDXComponent = true;"}},"pageContext":{"slug":"react-hooks-redux-toolkit-yet-another-chat-app"}},"staticQueryHashes":["1063564771","2248308066","2468095761"]}