Reading and Writing State

All hooks except useSendMessage automatically respect Save Container boundaries.

For example, usePushMessageIDs does not push messageIDs if inside a Save Container, the save container hook must be used to save.

Most of these examples will use the React useCallback hook in order to memoise the callback function. If the callback created creates a closure, pulling variables from outside its scope, add them to the dependencies. This is demonstrated below, but for further explanation see the official React documentation.

useHardwareState

useHardwareState is the most commonly used hook, it allows for accessing hardware state.

It takes an Accessor and returns the most up to date state, it will re-render the component when state updates.

import { useHardwareState } from '@electricui/components-core'
 
const ComponentThatTakesState = () => {
const brightnessPercentage = useHardwareState('brightness')
const brightnessPercentage: number | null
 
return <div>LED Brightness: {brightnessPercentage}%</div>
}

It takes a type argument that lets you specify the return type of the accessor. If you have strictly typed your state with the typedState.ts file, this isn't necessary, it will automatically be inferred when using functional Accessors.

Below is an example of the hook being passed a type argument of number, correctly typing the brightnessPercentage variable.

const ComponentThatTakesState = () => {
const brightnessPercentage = useHardwareState<number>('brightness')
const brightnessPercentage: number | null
 
return <div>LED Brightness: {brightnessPercentage}%</div>
}

Performance implications

The useHardwareState hook runs the Accessor every time any state update occurs, if the results are not referentially equal, a re-render will occur.

It is recommended to select the 'smallest possible slice' of state within this Accessor. If only the red channel is used from an RGB struct, only grab the red channel, instead of the entire RGB object.

Furthermore, a component re-render will cause all its children to re-render. It is not recommended to use this hook at the 'page' level, since it will force the whole page to re-render on change.

Instead, split the page up into multiple components that have specific concerns, so that the minimum re-renders occur.

useInterfaceState

The useInterfaceState hook is identical to the useHardwareState hook except it accesses the interface state tree.

It takes an Accessor and returns the most up to date state, it will re-render the component when state updates.

import { useInterfaceState } from '@electricui/components-core'
 
const ComponentThatTakesState = () => {
const brightnessPercentageOptimistic = useInterfaceState('brightness')
 
return <div>LED Brightness: {brightnessPercentageOptimistic}%</div>
}

A functional accessor can be used, if the state is strictly typed, the type of brightnessPercentageOptimistic will be correctly inferred.

const ComponentThatTakesState = () => {
const brightnessPercentageOptimistic = useInterfaceState(
uiState => uiState.brightness,
)
 
return <div>LED Brightness: {brightnessPercentageOptimistic}%</div>
}

useWriteState

The useWriteState hook is the simplest way to write custom state to hardware. If inside a save container, it will not write to hardware until the save button is pressed.

The hook returns a write function that can be called with both a Writer and whether to ack the message or not, as well as a CancellationToken.

In this example, a button without styling is re-implemented to set the red channel of the rgb variable to 3, acking the message.

Default timeouts will be passed to the upstream handler with the useDeadline hook.

import { useWriteState, useDeadline } from '@electricui/components-core'
import { useCallback } from 'react'
 
const CustomButton = () => {
const writeState = useWriteState()
const getDeadline = useDeadline()
 
const onClickCallback = useCallback(
() => {
const cancellationToken = getDeadline()
writeState(
state => {
state.rgb.red = 3
},
true,
cancellationToken,
)
},
[
/* closure dependencies, none in this case */
],
)
 
return <button onClick={onClickCallback}>Click me</button>
}

useQuery

The useQuery hook allows for requesting messageIDs imperatively.

The hook returns a function that can be called with a MessageID to query and a CancellationToken.

In this example, clicking this button requests the lit_status state.

import { useQuery, useDeadline } from '@electricui/components-core'
import { useCallback } from 'react'
 
const RequestNewDataComponent = () => {
const query = useQuery()
const getDeadline = useDeadline()
 
const onClickCallback = useCallback(
() => {
query('lit_status', getDeadline())
},
[
/* closure dependencies, none in this case */
],
)
 
return <button onClick={onClickCallback}>Request Data</button>
}

useCommitState

The useCommitState hook allows for writing updates only to the interface state.

The hook returns a write function that can be called with a Writer, returning an array of MessageIDs modified.

In this example, clicking the button optimistically changes the state of the red channel of the RGB messageID to 42.

It will also print The messageIDs changed were: ['rgb'] to console.

import { useCommitState } from '@electricui/components-core'
import { useCallback } from 'react'
 
const OptimisticWrite = () => {
const commit = useCommitState()
 
const onClickCallback = useCallback(
() => {
const modifiedMessageIDs = commit(state => {
state.rgb.red = 42
})
 
console.log('The messageIDs changed were:', modifiedMessageIDs)
},
[
/* closure dependencies, none in this case */
],
)
 
return <button onClick={onClickCallback}>Set red 42</button>
}

usePushMessageIDs

The usePushMessageIDs hook allows for pushing updates to hardware that have already been committed to the interface StateTree.

The hook returns a push function that can be called with an array of MessageID strings, whether to ack, and a CancellationToken, it returns a Promise of the writes.

In this example, clicking the button optimistically changes the state of the red channel of the RGB messageID to 42, writes the change to hardware, and then prints "updates pushed" to console.

import {
useCommitState,
usePushMessageIDs,
useDeadline,
} from '@electricui/components-core'
import { useCallback } from 'react'
 
const WriteRed42 = () => {
const commit = useCommitState()
const push = usePushMessageIDs()
const getDeadline = useDeadline()
 
const onClickCallback = useCallback(
() => {
const modifiedMessageIDs = commit(state => {
state.rgb.red = 42
})
 
push(modifiedMessageIDs, true, getDeadline()).then(() => {
console.log('updates pushed')
})
},
[
/* closure dependencies, none in this case */
],
)
 
return <button onClick={onClickCallback}>Set red 42</button>
}

useCommitStateStaged

The useCommitStateStaged hook is an advanced hook that will likely not need to be used unless developing custom components that take Writers with values.

The hook returns an array of two functions, one that generates and returns the 'staging environment' draft copy of the state that must be mutated, and a commit function that performs the change detection and commits it to the interface StateTree.

In this example, we create a custom textbox component that doesn't write to either the interface state tree or hardware until the save button is clicked.

Since the usePushMessageIDs hook respect save container boundaries, this won't actually write to hardware when inside a save container, the save container save button will have to be pressed.

import { useState, useCallback } from 'react'
import {
Accessor,
Draft,
useCommitStateStaged,
usePushMessageIDs,
useHardwareState,
useDeadline,
} from '@electricui/components-core'
 
interface LazilyWrittenTextBoxProps {
accessor: Accessor<string> // We know we need a string, so use a type argument to force the return of the Accessor to be a string
writer: (staging: Draft<ElectricUIDeveloperState>, value: string) => void
}
 
const LazilyWrittenTextBox = (props: LazilyWrittenTextBoxProps) => {
const [generateStaging, commit] = useCommitStateStaged()
const pushMessageIDs = usePushMessageIDs()
const getDeadline = useDeadline()
// grab initial state from hardware
const initialText = useHardwareState(props.accessor)
// maintain our own copy of the text
// we only update the main state tree on click of the save button
// if the initialText is null, set it to an empty string.
const [text, setText] = useState(initialText || '')
 
// When typing, update the local state.
const onChangeCallback = useCallback(
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
setText(event.target.value)
},
[
/* no closure dependencies */
],
)
 
// When the save button is clicked
const onSaveCallback = useCallback(() => {
const staging = generateStaging()
 
// Call the writer with our staging environment to mutate the state
props.writer(staging, text)
 
// commit the state
const modifiedMessageIDs = commit(staging)
 
// Push the modified messageIDs, and ack
pushMessageIDs(modifiedMessageIDs, true, getDeadline())
}, [
/* closure dependencies, this callback needs to change both if our writer changes, and when the text changes */
props.writer,
text,
])
 
// Our component is a text box with a save button below it.
return (
<div>
<textarea onChange={onChangeCallback} value={text} />
<button onClick={onSaveCallback}>save</button>
</div>
)
}

useSendMessage

The useSendMessage hook allows for writing messages directly to hardware. This is a very low level hook. The deviceID of the current context is automatically injected if one isn't set in the Message.

The hook returns a send function that can be called with a Message and a CancellationToken, returning a promise of the write.

import { useCallback } from 'react'
import { Message, DeviceID } from '@electricui/core'
import { useSendMessage, useDeadline } from '@electricui/components-core'
import { TYPES } from '@electricui/protocol-binary-constants'
 
const CustomSendMessage = () => {
const send = useSendMessage()
const getDeadline = useDeadline()
 
const onClickCallback = useCallback(
() => {
const message = new Message(
'messageID',
Buffer.from([0x00, 0x01, 0x02, 0x03]),
)
 
message.deviceID = '83290431' as DeviceID // a different deviceID to the current context may be set, otherwise one will be automatically injected
message.metadata.type = TYPES.CUSTOM_MARKER
message.metadata.ack = true
 
send(message, getDeadline())
},
[
/* closure dependencies, none in this case */
],
)
 
return <button onClick={onClickCallback}>Send very custom message</button>
}

useSendCallback

The useSendCallback hook is a convenience utility for sending callbacks to hardware.

The hook returns a send function that can be called with MessageID, a CancellationToken and an optionally whether to ack (defaulting to true), returning a promise of the write.

import { useCallback } from 'react'
import { useSendCallback, useDeadline } from '@electricui/components-core'
 
const CustomSendMessage = () => {
const send = useSendCallback()
const getDeadline = useDeadline()
 
const onClickCallback = useCallback(
() => {
send('cb_msgid', getDeadline())
},
[
/* closure dependencies, none in this case */
],
)
 
return <button onClick={onClickCallback}>Send very custom message</button>
}

Underlying Architecture

Electric UI uses Redux as the state management solution for all hardware state. Redux requires fully immutable objects in order to do referential equality comparisons at each level of the tree to avoid a deep comparison (a very expensive operation) per change.

However, writers force mutation of the state tree, presumably breaking the immutability guarantee.

If the state argument is inspected, the type can be seen to be Draft<ElectricUIDeveloperState>.

Under the hood, the state tree is wrapped in a series of Proxy objects which lets Electric UI be notified of the exact leaves of the tree that are mutated. An immutable copy is then generated from this list of changes.

In advanced settings where the same state is written first as a non acked message, then again as an acked message, seemingly incorrect behaviour will occur in some circumstances.

The second write won't produce any changes, given that the state is identical, as a result there will be no message written to the hardware at all.

If writing custom components, and this problem arises, the useCommitState hook's function returns the list of messageIDs modified each write. Use the messageIDs from the first write and push them as well upon the second write.