Solid JS is a brand new, blazingly fast framework created by @ryansolid. It takes reactivity and developer experience to a whole new level. Visually, It looks like react but is a lot better and performs a lot better.
Creating a real-time chat app using firebase and solid js will really help us understand more about solid js and how it works.
Note: This tutorial assumes you already have a firebase project setup. If you don't, follow this guide.
About the Application
We are going to be creating a single page group-chatting application. Any client can go on their browser and join the chat. It is not much but will help us understand some key things about solid Js.
Bootstrapping the Application
To bootstrap the solid.js application, we will be using the terminal command
npx degit solidjs/templates/ts firebase-chat-app
cd firebase-chat-app
You can go ahead and install the dependencies and run the app with your favorite package manager. I'm using yarn.
yarn install
yarn add firebase
yarn dev
N.B: The bootstrapped application setup looks a bit similar to react except this is a different framework: Solid.js
Firebase Setup
To connect firebase to our application we will be using the firebase sdk.
To start off, we will be replacing src/index.tsx
with this.
import { render } from 'solid-js/web';
import App from './App';
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
// Include your firebase config
const firebaseConfig = {};
export const app = initializeApp(firebaseConfig);
render(() => <App />, document.getElementById('root') as HTMLElement);
Next, We will need to create some hooks to help us use the firebase better. We will be creating two hooks. useAuth and useFirestore
useAuth:
Firstly we will create a useAuth.ts
file a new hooks directory src/hooks/useAuth.ts
. We will then add the following to the useAuth.ts
file:
export default function useAuth(app: FirebaseApp) {
const [authState, setAuthState] = createStore<IAuthState>({
user: null,
error: null,
loading: true,
});
const auth = getAuth(app);
// Setup Listeners
auth.onAuthStateChanged(
(user) => {
user &&
setAuthState(
produce((state) => {
state.user = user;
state.loading = false;
})
);
},
(error) => {
error &&
setAuthState(
produce((state) => {
state.error = error;
state.loading = false;
})
);
},
() => {
setAuthState(
produce((state) => {
state.loading = false;
})
);
}
);
// Sign in if not already signed in
signInAnonymously(auth).then((data) => {
setAuthState(
produce((state) => {
state.user = data.user;
})
);
});
return authState;
}
useAuth
will enable us to get the current auth state of the person logged in. It also help us sign in anonymously if we are not already logged in.
useFirestore
The useFirestore hook is created to help in adding records to a firestore collection, read data(in realtime) etc.
First of all, we create a useFirestore.ts
file in the hooks directory. The code below is what is used in the hook file:
export default function useFirestore(app: FirebaseApp) {
const db = getFirestore(app);
const authState = useAuth(app);
async function addData(collectionName: string, data: Record<string, any>) {
const doc = await addDoc(collection(db, collectionName), {
...data,
from: authState?.user?.uid,
created: Timestamp.now(),
});
console.log('doc', doc);
}
function getData(collectionName: string) {
const [data, setData] = createSignal<DocumentData[]>([]);
getDocs(collection(db, collectionName)).then((records) => {
setData(records.docs.map((doc) => doc.data()));
});
const q = query(collection(db, collectionName), orderBy('created', 'asc'));
onSnapshot(q, (snapshot) => {
setData(snapshot.docs.map((doc) => doc.data()));
});
return data;
}
return { db, addData, getData };
}
As you can see, we are creating two functions: One to add data to a given collection and another to get data from a specified collection. The addData function also sets up a listener to firestore to get the chats in realtime.
Building the app
Before We dive into making realtime chat work, we will create the ui for this using solid js.
First of all, we will have only one page which is the chat screen page. This is going to be in the App component. The main div wraps the title, messages and message input box.
const App: Component = () => {
const [messageTerm, setMessageTerm] = createSignal('');
const [messageId, setMessageId] = createSignal('vision');
return (
<div class={styles.App}>
<h1>Message the Group</h1>
<div class={styles.App__messages}>
<For each={messages()}>
{(message) => (
<div
class={
message.from == messageId()
? styles.App__messageLocal
: styles.App__messageForeign
}
>
{message.message}
</div>
)}
</For>
</div>
<div class={styles.App__input}>
<textarea
placeholder='Message the group'
onKeyUp={(e) => setMessageTerm(e.currentTarget.value)}
/>
<button class={styles.App__button}>Send</button>
</div>
</div>
);
};
There are a couple of things to notice about the solid.js code and its difference to react.
Usage of For Component instead of Mapping
Signals
Lifecycle hooks
Among others.
Putting it all together
Currently the messages are hardcoded and the send button does nothing. This next step is to add firebase functionality to our chat application.
Firstly, We will need to extract the chat into a new component.
Create a new file
src/components/Chat.tsx
. Add the chat screen code into it that was previously in App component.Make sure imports are pointing to the right place.
Now that the chat is in a new file, we can replace App.tsx
with
const App: Component = () => {
const authState = useAuth(app);
return (
<Switch>
<Match when={authState.loading}>
<p>Loading...</p>
</Match>
<Match when={authState.error}>
<p>Error</p>
</Match>
<Match when={authState.user}>
<Chat />
</Match>
</Switch>
);
};
The app component now uses our custom useAuth
hook to listen to the user's current auth state and renders a loading, error text or the chat screen based on the auth state.
Note: The Switch component is used for conditional rendering in solid when there are multiple conditions.
Next up, we will add firestore to our chat component. To achieve this we need to do the following:
Firstly, we get the data from firestore using our custom hooks:
const { addData, getData } = useFirestore(app);
const data = getData('chats');
const authState = useAuth(app);
The first two lines are for getting the data while the third is for the getting the current auth state.
Next, we make the messages dynamic. Previously the messages were hard coded but since we now have the firestore messages, we can replace the hardcoded messages with realtime messages as follows:
<For each={data()}>
{(message) => (
<div
class={
message.from == authState?.user?.uid
? styles.App__messageLocal
: styles.App__messageForeign
}
>
{message.message}
</div>
)}
</For>
Two things are happening in the code block above. Firstly we are replacing hardcoded messages array with the actual data. Secondly, the message source is gotten by comparing the message(firestore) from field with our unique id.
Finally, we can add an onClick handler to our send button to send the messages. The button with the onclick handler is now
<button class={styles.App__button} onClick={handleSubmit}>
Send
</button>
The handleSubmit function implementation just adds sends the message to firestore. It is implemented like this:
function handleSubmit() {
addData('chats', {
message: messageTerm(),
});
submit_input.value = '';
}
We are using the addData function gotten from our useFirestore hook. submit_input
is a ref to our textarea.
After doing all these, our website now looks like(with my styles)
You can test it by opening two different browser clients and chatting.
Conclusion
Creating this project has really helped us to understand solidjs concepts more like stores, signals, produce etc.
The deployed version can be found on https://realtime-solid-chat-app.web.app/. All the source code can be found on github.
Thanks. Buy me a coffee