React

React Slack Clone App

Introduction

In this guide, we will keep exploring the Parse React hook in useful situations by creating a Slack Clone App. The App will have basic features such as signup/login, channels, and real-time chat. In addition, the React components highlight the usefulness of real-time notifications using Live Query, User management capabilities, and the flexibility to perform queries (relational).


At any moment you can quickly deploy a slack clone app on Vercel:

Prerequisites

To complete this tutorial, you will need:

Goal

To build a Slack clone application on React using the @parse/react hook.

Step 1 - Creating your Parse app from a template

This application comprises two database classes: Channel and Message, containing pointers to the Parse User class. Instead of creating the app and database classes from scratch, let’s clone an existing template at Back4App Database Hub. Click on the “Clone Database” button and proceed with logging in and creating your app. For more details on how to clone please check the clone App guide.

React Back4App

Now your App has the complete backend structure necessary to create your Slack Clone on Back4App.

Step 2 - Enabling Live Query

Now that you have created the App and classes, you need to enable the live query(real-time). Go to your Back4App dashboard and navigate to App Settings > Server Settings > Server URL and Live Query. After activating your Back4App subdomain, you can then activate Live Query by selecting the classes Message and Channel. After that, save the changes.

React Back4App

The URL above is your Live Query URL make sure to copy it in order to properly initialize the Parse hook.

Let’s now dive into the core React components: ChannelList, MessageList, and Home.

Step 3 - The List components

The ChannelList and MessageList components use the @parse/react hook to retrieve the User data through Live Query. They have the same pattern structure as in the React LiveChat guide. They are instantiated with initial parameters (retrieved via the props object) that dynamically compose their Live Query query. Take a look at their queries and how the classes are related to each other:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Note that Parse.Object coming from props need to be changed into pointers
// to be able to be used by Parse, since React changes the object structure
// when passing down parameters to child components

// channelList.js

const ownerQuery = new Parse.Query("Channel");
ownerQuery.equalTo("owner", props.currentUser.toPointer());
const membersQuery = new Parse.Query("Channel");
membersQuery.containedIn("members", [props.currentUser.toPointer()]);
// Creates the OR query
const parseQuery = Parse.Query.or(ownerQuery, membersQuery);
// Set results ordering
parseQuery.ascending("name");
// Include all pointer fields
parseQuery.includeAll();

// messageList.js

const parseQuery = new Parse.Query("Message");
// Get messages that involve both nicknames
parseQuery.equalTo("channel", props.currentChannel.toPointer());
// Set results ordering
parseQuery.ascending("createdAt");
// Include nickname fields, to enable name getting on list
parseQuery.includeAll();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Note that Parse.Object coming from props need to be changed into pointers
// to be able to be used by Parse, since React changes the object structure
// when passing down parameters to child components

// channelList.tsx

// This query is a composite OR one, combining the results of both
const ownerQuery: Parse.Query = new Parse.Query("Channel");
ownerQuery.equalTo("owner", props.currentUser.toPointer());
const membersQuery: Parse.Query = new Parse.Query("Channel");
membersQuery.containedIn("members", [props.currentUser.toPointer()]);
// Creates the OR query
const parseQuery: Parse.Query = Parse.Query.or(ownerQuery, membersQuery);
// Set results ordering
parseQuery.ascending("name");
// Include all pointer fields
parseQuery.includeAll();

// messageList.tsx

const parseQuery: Parse.Query = new Parse.Query("Message");
// Get messages that involve both nicknames
parseQuery.equalTo("channel", props.currentChannel.toPointer());
// Set results ordering
parseQuery.ascending("createdAt");
// Include nickname fields, to enable name getting on list
parseQuery.includeAll();

These queries will be running every time there is a change in the classes data, so if another user in the channel sends a message, you will see it appearing there in real-time.

Step 4 - The Home component

The Home component acts as the main application screen, in which the list components are conditionally rendered and instantiated when needed. You can find below the component code. Take a look at the functions for creating channels and inviting users to them.

Home.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import React, { useEffect, useState } from "react";
import "./App.css";
import { Modal } from "antd";
import { useHistory } from "react-router-dom";
import Parse from "parse";
import { ChannelList } from "./ChannelList";
import { MessageList } from "./MessageList";
import { MemberList } from "./MemberList";

export const Home = () => {
  const history = useHistory();

  // State variables holding input values and flags
  const [currentUser, setCurrentUser] = useState(null);
  const [isCreateChannelModalVisible, setIsCreateChannelModalVisible] =
    useState(false);
  const [createChannelInput, setCreateChannelInput] = useState("");
  const [currentChannel, setCurrentChannel] = useState(null);

  // This effect hook runs at every render and checks if there is a
  // logged in user, redirecting to Login screen if needed
  useEffect(() => {
    const checkCurrentUser = async () => {
      try {
        const user = await Parse.User.currentAsync();
        if (user === null || user === undefined) {
          history.push("/");
        } else {
          if (currentUser === null) {
            setCurrentUser(user);
          }
        }
        return true;
      } catch (_error) {}
      return false;
    };
    checkCurrentUser();
  });

  // Logout function
  const doLogout = async () => {
    // Logout
    try {
      await Parse.User.logOut();
      // Force useEffect execution to redirect back to Login
      setCurrentUser(null);
      return true;
    } catch (error) {
      alert(error);
      return false;
    }
  };

  // Makes modal visible
  const showCreateChannelModal = () => {
    setIsCreateChannelModalVisible(true);
  };

  // Clear input and hide modal on cancel
  const handleCreateChannelModalCancel = () => {
    setCreateChannelInput("");
    setIsCreateChannelModalVisible(false);
  };

  // Creates a channel based on input from modal
  const doCreateChannel = async () => {
    const channelName = createChannelInput;

    if (channelName === "") {
      alert("Please inform your new channel name!");
      return false;
    }

    // Creates a new Parse.Object instance and set parameters
    const Channel = new Parse.Object("Channel");
    Channel.set("name", channelName);
    Channel.set("owner", currentUser);
    // Members is an array of Parse.User objects, so .add() should be used to
    // concatenate the value inside the array
    Channel.add("members", currentUser);

    // Clears input value and hide modal
    setCreateChannelInput("");
    setIsCreateChannelModalVisible(false);

    try {
      // Save object on Parse server
      const saveResult = await Channel.save();
      // Set the created channel as the active channel,
      // showing the message list for this channel
      setCurrentChannel(saveResult);
      alert(`Success on creating channel ${channelName}`);
      return true;
    } catch (error) {
      alert(error);
      return false;
    }
  };

  // Changes the active channel and shows the message list for it
  // This is called using a callback in the ChannelList component
  const doSelectChannel = (channel) => {
    setCurrentChannel(null);
    setCurrentChannel(channel);
  };

  // Settings current channel to null hides the message list component
  // This is called using a callback in the MessageList component
  const doClearCurrentChannel = () => {
    setCurrentChannel(null);
  };

  return (
    <div className="grid">
      <div className="organizations">
        <div className="organization">
          <picture className="organization__picture">
            <img
              className="organization__img"
              src="https://scontent.fsqx1-1.fna.fbcdn.net/v/t1.6435-9/29136314_969639596535770_8356900498426560512_n.png?_nc_cat=103&ccb=1-5&_nc_sid=973b4a&_nc_ohc=D9actPSB8DUAX-zaA7F&_nc_ht=scontent.fsqx1-1.fna&oh=96679a09c5c4524f0a6c86110de697b6&oe=618525F9"
              alt=""
            />
          </picture>
          <p className="organization__title">Back4App</p>
        </div>
        <button className="button-inline" onClick={doLogout}>
          <svg
            className="button-inline__icon"
            xmlns="http://www.w3.org/2000/svg"
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          >
            <polyline points="9 10 4 15 9 20"></polyline>
            <path d="M20 4v7a4 4 0 0 1-4 4H4"></path>
          </svg>
          <span className="button-inline__label">Log out</span>
        </button>
      </div>
      <div className="channels">
        {/* Action buttons (new channel and logout) */}
        <div>
          <Modal
            title="Create new channel"
            visible={isCreateChannelModalVisible}
            onOk={doCreateChannel}
            onCancel={handleCreateChannelModalCancel}
            okText={"Create"}
          >
            <>
              <label>{"Channel Name"}</label>
              <input
                type={"text"}
                value={createChannelInput}
                placeholder={"New Channel Name"}
                onChange={(event) => setCreateChannelInput(event.target.value)}
              ></input>
            </>
          </Modal>
        </div>
        <div className="channels-header" onClick={showCreateChannelModal}>
          <p className="channels-header__label">Channels</p>
          <svg
            className="channels-header__icon"
            xmlns="http://www.w3.org/2000/svg"
            height="24px"
            viewBox="0 0 24 24"
            width="24px"
            fill="#000000"
          >
            <path d="M0 0h24v24H0z" fill="none" />
            <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
          </svg>
        </div>
        {/* Channel list component, instantiated only when the user is successfully fetched */}
        {currentUser !== null && (
          <ChannelList
            currentUser={currentUser}
            selectChannelCallback={doSelectChannel}
          />
        )}
      </div>
      <div className="messages">
        {/* Message list component, instantiated only when there is a selected channel */}
        {currentUser !== null && currentChannel !== null && (
          <MessageList
            currentUser={currentUser}
            currentChannel={currentChannel}
            closeChannelCallback={doClearCurrentChannel}
          />
        )}
      </div>
      <div className="info">
        {/* Member list component, instantiated only when there is a selected channel */}
        {currentUser !== null && currentChannel !== null && (
          <MemberList
            currentUser={currentUser}
            currentChannel={currentChannel}
            closeChannelCallback={doClearCurrentChannel}
          />
        )}
      </div>
    </div>
  );
};

Home.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import React, { useEffect, useState, FC, ReactElement } from 'react';
import './App.css';
import { Modal } from 'antd';
import { useHistory } from 'react-router-dom';
import Parse from 'parse';
import { ChannelList } from './ChannelList';
import { MessageList } from './MessageList';
import { MemberList } from './MemberList';

export const Home: FC<{}> = (): ReactElement => {
  const history = useHistory();

  // State variables holding input values and flags
  const [currentUser, setCurrentUser] = useState<Parse.User | null>(null);
  const [isCreateChannelModalVisible, setIsCreateChannelModalVisible] = useState(false);
  const [createChannelInput, setCreateChannelInput] = useState('');
  const [currentChannel, setCurrentChannel] = useState<Parse.Object | null>(null);

  // This effect hook runs at every render and checks if there is a
  // logged in user, redirecting to Login screen if needed
  useEffect(() => {
    const checkCurrentUser = async (): Promise<Boolean> => {
      try {
        const user: (Parse.User | null) = await Parse.User.currentAsync();
        if (user === null || user === undefined) {
          history.push('/');
        } else {
          if (currentUser === null) {
            setCurrentUser(user);
          }
        }
        return true;
      } catch (_error: any) {}
      return false;
    }
    checkCurrentUser();
  });

  // Logout function
  const doLogout = async (): Promise<Boolean> => {
    // Logout
    try {
      await Parse.User.logOut();
      // Force useEffect execution to redirect back to Login
      setCurrentUser(null);
      return true;
    } catch (error: any) {
      alert(error);
      return false;
    }
  };

  // Makes modal visible
  const showCreateChannelModal = (): void => {
    setIsCreateChannelModalVisible(true);
  }

  // Clear input and hide modal on cancel
  const handleCreateChannelModalCancel = (): void => {
    setCreateChannelInput("");
    setIsCreateChannelModalVisible(false);
  }

  // Creates a channel based on input from modal
  const doCreateChannel = async (): Promise<boolean> => {
    const channelName: string = createChannelInput;
    
    if (channelName === '') {
      alert("Please inform your new channel name!");
      return false;
    }

    // Creates a new Parse.Object instance and set parameters
    const Channel: Parse.Object = new Parse.Object("Channel");
    Channel.set('name', channelName);
    Channel.set('owner', currentUser);
    // Members is an array of Parse.User objects, so .add() should be used to
    // concatenate the value inside the array
    Channel.add('members', currentUser);

    // Clears input value and hide modal
    setCreateChannelInput("");
    setIsCreateChannelModalVisible(false);

    try {
      // Save object on Parse server
      const saveResult: Parse.Object = await Channel.save();
      // Set the created channel as the active channel,
      // showing the message list for this channel
      setCurrentChannel(saveResult);
      alert(`Success on creating channel ${channelName}`);
      return true;
    } catch (error: any) {
      alert(error);
      return false;
    }
  }

  // Changes the active channel and shows the message list for it
  // This is called using a callback in the ChannelList component
  const doSelectChannel = (channel: Parse.Object): void => {
    setCurrentChannel(null);
    setCurrentChannel(channel);
  }

  // Settings current channel to null hides the message list component
  // This is called using a callback in the MessageList component
  const doClearCurrentChannel = (): void => {
    setCurrentChannel(null);
  }

  return (
    <div className="grid">
      <div className="organizations">
        <div className="organization">
          <picture className="organization__picture">
            <img className="organization__img" src="https://scontent.fsqx1-1.fna.fbcdn.net/v/t1.6435-9/29136314_969639596535770_8356900498426560512_n.png?_nc_cat=103&ccb=1-5&_nc_sid=973b4a&_nc_ohc=D9actPSB8DUAX-zaA7F&_nc_ht=scontent.fsqx1-1.fna&oh=96679a09c5c4524f0a6c86110de697b6&oe=618525F9" alt="" />
          </picture>
          <p className="organization__title">Back4App</p>
        </div>
        <button className="button-inline" onClick={doLogout}>
          <svg className="button-inline__icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 10 4 15 9 20"></polyline><path d="M20 4v7a4 4 0 0 1-4 4H4"></path></svg>
          <span className="button-inline__label">Log out</span>
        </button>
      </div>
      <div className="channels">
        {/* Action buttons (new channel and logout) */}
        <div>
          <Modal
            title="Create new channel"
            visible={isCreateChannelModalVisible}
            onOk={doCreateChannel}
            onCancel={handleCreateChannelModalCancel}
            okText={'Create'}
          >
            <>
              <label>{'Channel Name'}</label>
              <input
                type={"text"}
                value={createChannelInput}
                placeholder={"New Channel Name"}
                onChange={(event) => setCreateChannelInput(event.target.value)}
              ></input>
            </>
          </Modal>
        </div>
        <div className="channels-header" onClick={showCreateChannelModal}>
          <p className="channels-header__label">Channels</p>
          <svg className="channels-header__icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
        </div>
        {/* Channel list component, instantiated only when the user is successfully fetched */}
        {currentUser !== null && (
          <ChannelList
            currentUser={currentUser}
            selectChannelCallback={doSelectChannel}
          />
        )}
      </div>
      <div className="messages">
        {/* Message list component, instantiated only when there is a selected channel */}
        {currentUser !== null && currentChannel !== null && (
          <MessageList
            currentUser={currentUser}
            currentChannel={currentChannel}
            closeChannelCallback={doClearCurrentChannel}
          />
        )}
      </div>
      <div className="info">
        {/* Member list component, instantiated only when there is a selected channel */}
        {currentUser !== null && currentChannel !== null && (
          <MemberList
            currentUser={currentUser}
            currentChannel={currentChannel}
            closeChannelCallback={doClearCurrentChannel}
          />
        )}
      </div>
    </div>
  );
};

This approach of dynamically instantiating the Live Query components allows us to reuse them whenever the user changes the active channel, creates a new one, sends a message, etc. Here is how the complete App will look.

React Back4App

Step 5 - Deploy on Vercel


At any time you can deploy the application on vercel by clicking on the link bellow:

Make sure you have your Application ID, Client Key and LiveQuery URL. For the keys you can go to App Settings -> Security & Keysand then copy. For the Live Query URL you can go to Step 2 and copy it.

Conclusion

At the end of this guide, you learned more about using the Parse React hook for live queries in Parse and how to use Back4App’s Database Hub.