Full Stack Type-Safe State Management With React, Bun, and Bun Nook Kit Websockets

Brandon Schabel
Nookit Dev
Published in
7 min readAug 20, 2023
AI Generated — Inspiration: Data Tree, State Managment

State management is a crucial aspect of developing functional and responsive web applications. It involves keeping track of how an application’s state changes over time and rendering the appropriate UI to reflect these changes. In real-time applications, managing state becomes even more challenging due to the constant, instantaneous updates required.

I wrote this originally when BNK was an early project so if anyone ever needs to reference this, I’d be happy to update it, but it is a low read story.

However, one major pain point that developers encounter, especially hobbyists and open-source contributors, relates to the lack of appropriate tools for quick prototyping. Current available tools are generally optimized for enterprise applications, focusing more on scalability to millions of users, and leveraging a plethora of services. This approach often requires a high level of dependence on service platforms, which not only have large marketing budgets, but also much to gain from widespread reliance on their services.

The need for simpler, efficient, and robust tools for state management in web application development cannot be overstated. It’s critical to have tools that allow developers to build and prototype applications swiftly, without burdening them with scalability concerns at the early stages of development. The focus should be on delivering functional prototypes that can attract initial users, rather than optimizing for massive scale prematurely.

This is where BNK comes in. Designed with a ‘use-what-you-need’ philosophy, BNK aims to address this gap by providing a comprehensive base library for building full-stack applications quickly and efficiently. With BNKs, developers can manage all aspects of their application, from the database to client-side authentication, on the same server. This approach ensures fast response times without the complexities of serverless and edge technologies, which, while they have their place, are more suited to worldwide high-throughput applications. U Tools empowers developers to own their application’s development completely, placing control back into their hands for a more streamlined and effective development process.

Implementing Real-time State Management with BNK

One of BNK’s standout features is its WebSocket state implementation. To illustrate its functionality, let’s walk through a sample project.

Our application will consist of a simple server and a client. The server will keep track of some state, and a client will update this state via WebSocket communication.

Since I was to focus purely on the feature, at the end of the post there is a link to the project if you want to try it yourself

As of writing versions:

Bun — 1.0.12

bnkit(core package) — 0.4.35

@bnk/react — 0.4.35

The Server

Firstly, we initialize our server handlers using BNK’s createServerFactory function, specifying our WebSocket paths, enabling CORS and Body Parser middlewares, and configuring CORS for our client.

Next, we define a base route for our server. Any request to this base route will return a simple “Hello World” message.

Following this, we create a state machine/handler using the createWSStateMachine function. This handler will manage WebSocket communication between the client(s) and the server.

Lastly, we start our server, specifying the port and the WebSocket handler. Below is the server code:

server/index.tsx:

import {
createServerFactory,
createWSStateMachine,
} from "bnkit/server";
import { defaultState } from "../client/src/socket-context";
// first we need to initialize our server handlers, U Tools is largely
// based on factory functions for it's core modules, here we configure our
// websocket paths, whether to enable the U Tools CORS and Body Parser middlewares, as well as the CORS config for our client
const { start, route } = createServerFactory({
wsPaths: ["/state"],
enableBodyParser: true,
cors: {
allowedOrigins: ["http://localhost:5173"],
},
});
// Base route for our server for a sanity check
const { onRequest: onBaseRequest } = route("/");
// when the home route("/") is requested then we just send "Hello World"
// along with the clients requested method
onBaseRequest(async ({ request }) => {
return new Response(`Hello World ${request.method}`);
});
// below we create the state machine/handler which will be completely handled
// via websocket communication between the client(s)
const { websocketHandler, onStateChange, control, state } =
createWSStateMachine(defaultState);
// listen for state changes, along with a callback of the updated stated
// value, everything is completely typed and inferred from our "defaultState"
// object, so it will error if you change the "count" string and let you know
// that is not a valid state value, nor will the function work
onStateChange("count", (count) => {
control.updates.set(state.updates + 1);
});
// on each count or input state change we want to keep track of the total updates for this demo
onStateChange("input", (input) => {
control.updates.set(state.updates + 1);
});
start({
websocket: websocketHandler,
port: 8080,
verbose: true,
});

Next we will get our client setup, but below is an example of the output you’d see if you were to console log the values once the client is setup.

{
key: "updates",
updater: 26,
newValue: 26
}
{
newValue: 26,
updatedStateData: {
key: "updates",
value: 26
}
}
{
newValue: "what is going o",
updatedStateData: {
key: "input",
value: "what is going o"
}
}

The Client

On the client side, we first define our state shape. This is important as all future state manipulations will be inferred from this initial state definition.

Next, we define a useWebsocketState hook that connects to our WebSocket and provides us with the control to manipulate the state and the current state.

We then create a context to provide a single source of truth for our state and controls, which prevents the need for multiple WebSocket connections. To note — I’ll likely provide the type-safe context directly from the @bnk/react package in the future.

client/src/socket-context.tsx:

import { Dispatchers } from "bnkit/server/create-web-socket-state-machine";
import { useServerState } from "bnkit/plugins/react/use-server-state";
import { ReactNode, createContext, useContext } from "react";

// define our state shape, this is important if you don't define your state with
// typescript, since everything is inferred you don't need to necessarily
// configure your own state type
export const defaultState = {
count: 0,
input: "",
updates: 0,
};
// Create the type based on the state object
export type StateType = typeof defaultState;
// Create the dispatcher types based on the state type structure
type StateDispatchers = Dispatchers<StateType>;
export const useWebsocketState = () => {
// connect to our websocket, and get the controller and state.
// the control will be used to manipulate the state and the state
// is readonly
const { control, state } = useServerState<StateType>({
defaultState,
url: "ws://localhost:8080/state",
});
return {
control,
state,
};
};
// Create the react context so we don't need to create so many websocket connections, as well as having a single source of truth for the state and controls
export const SocketAppContext = createContext<{
control: StateDispatchers;
state: StateType;
}>({
state: defaultState,
control: {} as StateDispatchers,
});
export const SocketContextProvider = ({
children,
}: {
children: ReactNode;
}) => {
const appState = useWebsocketState();
return (
<SocketAppContext.Provider value={appState}>
{children}
</SocketAppContext.Provider>
);
};
// finally use the types from the context (which exactly match
// the return type useWebsocketState)
export const useAppState = () => {
return useContext(SocketAppContext);
};

client/src/app.tsx:

import { ChangeEvent } from "react";
import { useAppState } from "./socket-context";

export function App() {
// use the context hook that we created for our control and state
const { control, state } = useAppState();
const handleInput = (event: ChangeEvent<HTMLInputElement>) => {
// when the input changes, we grab the event target value and
// we pass that to the control, notice, how we are not controlling the
// input at all. We are only listening
to the changes. WHen the page is loaded
// we pass in the states value via "defaultValue"
const inputElement = event.target as HTMLInputElement;
control.input.set(inputElement.value);
};
return (
<div>
<div>
{/* numbers have helper functions such as increment, arrays
have built in push, pop, and insert handlers, which
take care of the necessary state modifications to prevent
unexpected updates
*/}
<button onClick={() => control.count.decrement()}>-</button>
{/* this state cound will update as soon as the value is received,
from the websocket, which is basically instead*/}
{state.count}
<button onClick={() => control.count.increment()}>+</button>
</div>
<div>
<label>
What is your name?
<div>
{/* Since we are not controlling the input value at all, the update
will be instant to the user, and the websocket updates happen in the background
*/}
<input onChange={handleInput} defaultValue={state.input} />
</div>
</label>
<div>
<span>Hello {state.input}!</span>
</div>
<div>
<span>Number Of Updates {state.updates}!</span>
</div>
</div>
</div>
);
}
export default App;


Our beautiful UI:

So🐟ticated UI

Key Takeaways

The real-time state management feature of BNK offers several benefits:

  1. Seamless Integration: Bun Nook Kit’ WebSocket state management allows for easy integration of real-time state updates into any application.
  2. Efficient State Handling: Rather than manually managing state updates and notifications, Bun Nook Kit handles this automatically, reducing complexity and improving performance.
  3. High Flexibility: The system allows for a wide range of state manipulations, covering various use cases.

Real-time state management in web applications can be greatly simplified using BNK. Its WebSocket implementation provides a robust, efficient, and flexible solution, eliminating much of the complexity associated with real-time state updates. Whether you’re a hobbyist embarking on a small project or a seasoned developer tackling a larger application, U Tools can be a valuable addition to your development toolkit.

GitHub project: https://github.com/brandon-schabel/u-tools-websocket-demo

Bun Nook Kit Project:

https://github.com/brandon-schabel/bun-nook-kit

--

--

Brandon Schabel
Nookit Dev

Previously SWE at Stats Perform. Open Source contributor who writes about my work - exploring new tech like Bun and developing Bun Nook Kit.