Using recoil with You.i Engine One

Managing shared data across multiple components can be a challenge for applications. Across many apps I have worked on, I have seen variations or combinations of prop-drilling, use of redux, and publish-subscribe models shared between components. All of the above get the job done but can potentially suffer performance hits as re-renders traverse the tree in order to update the individual components interested in the data.

Recoil is a small library provided by Facebook that attempts to solve specific use cases with regards to sharing data across components that they may not be related in the React tree.

I recommend watching the following excellent introduction to the topic here.

Support with You.i Engine One

Recoil is supported by You.i Engine One as of 6.0, and the first use case I thought made sense to demonstrate was the consumption of Video Callbacks and how they are handled for controls and other functionality.

First let’s take a stab at how most video apps may be laid out:

In a common video app you are going to come across a number of elements like the above, and each of them is driven by the information provided by the video playing in the background.

Items (1), (2) and (3) are driven by the contentDuration and currentTime of the video playing. Item (4), a primary action, could be toggled by the mediaState of the video (and double as a buffering indicator) and the meta data component of (5) could either be driven by id3 tags, or even something simpler, such as the time remaining based off currentTime and contentDuration, like the items above.

If we were to structure this screen with a typical top down approach where we capture the state at the root and drill it down to the components that require it, it may look like the following:

const Screen = () => {
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [mediaState, setMediaState] = useState(null):
return (
<>
<Video
...
onDurationChanged={(value) => setDuration(value)}
onCurrentTimeUpdated={(value) => setCurrentTime(value)}
onStateChanged={ (playerState) => {
setMediaState({
playbackState: playerState.nativeEvent.playbackState,
mediaState: playerState.nativeEvent.mediaState
})
}
/>
<View style={{ position: 'absolute' }}>
<ControlsOverlay
duration={duration}
currentTime={currentTime}
mediaState={mediaState}
/>
</View>
<View style={{ position: 'absolute', top: 0, left: 10 }}>
<UpNext currentTime={currentTime} />
</View>
)
}
const ControlsOverlay = ({duration, currentTime, mediaState}) => {
return (
<>
<PrimaryAction mediaState={mediaState} />
<View style={{ display: 'flex', flexDirection: 'row' }}>
<TimeElapsed currentTime={currentTime} />
<Scrollbar currentTime={currentTime} duration={duration} />
<TimeLeft currentTime={currentTime} duration={duration} />
</View>
)
}

While the above works fine, and may be appropriate for the structure of the application, recoil provides the ability to remove some of this passing of props and instead have the individual components directly access some shared data state that is updated by the source of truth itself, the video player.

We can define these items of shared state, or atoms, anywhere in our codebase:

import { atom } from 'recoil';const VideoMediaState = atom({
key: "videoMediaState",
default: { playbackState: null, mediaState: null }
});
const VideoDuration = atom({
key: "videoDuration",
default: 0
});
const VideoCurrentTime = atom({
key: "videoCurrentTime",
default: 0
});

These atoms of shared state can now be accessed and set from the Video callbacks. We can remove the passing of props to child components as well, as these components themselves will be able to directly access the atoms as well.

import { useRecoilState } from 'recoil';const Screen = () => {
return (
<>
<VideoControl />
<View style={{ position: 'absolute' }}>
<ControlsOverlay />
</View>
<View style={{ position: 'absolute', top: 0, left: 10 }}>
<UpNext />
</View>
)
}
const VideoControl = () => {
const [, setDuration] = useRecoilState(VideoDuration);
const [, setCurrentTime] = useRecoilState(VideoCurrentTime);
const [, setMediaState] = useRecoilState(VideoMediaState):

return (
<Video
...
onDurationChanged={(value) => setDuration(value)}
onCurrentTimeUpdated={(value) => setCurrentTime(value)}
onStateChanged={ (playerState) => {
setMediaState({
playbackState: playerState.nativeEvent.playbackState,
mediaState: playerState.nativeEvent.mediaState
})
}
/>
}
const ControlsOverlay = () => {
return (
<>
<PrimaryAction />
<View style={{ display: 'flex', flexDirection: 'row' }}>
<TimeElapsed />
<Scrollbar />
<TimeLeft />
</View>
)
}

Our components that directly use the data, such as TimeElapsed, can access it directly themselves:

const TimeRemaining = () => {
const [videoPosition] = useRecoilState(VideoCurrentTime);
const [videoDuration] = useRecoilState(VideoDurationState);
function getTimestamp(videoPosition, videoDuration) {
// some function to display timestamp correctly
return `00:00:00`;
}
return (
<Text style={styles.h1}>
{getTimestamp(videoPosition, videoDuration)}
</Text>
);
};

note: I have been made aware of the different accessors with the recoil state library, such as useSetRecoilState , I have left the syntax above to demonstrate it’s similarities to the useState implementation you commonly see.

Recoil vs useSelector

This pattern may seem similar, as it is closer to how it would be done using redux and the useSelector hook, however the primary difference here between using recoil and redux would be

(1) No need to register a bunch of reducers, even with the simplified abilities of the redux-toolkit slices.

(2) No need to dispatch events and payloads, which would be processed by all registered reducers on each dispatch, and have the tree marked dirty wherever a reducer is impacted downwards

(3) The re-rendering is isolated to the components that use the recoil state atom allowing you to consume events that occur very frequently (the updating of currentTime for example) without impacting the rest of the tree.

Results

In the above sample, the Time Elapsed, SeekBar, and Time Remaining components are all connecting to the atom independently as described above.

The code for the above can be found here.

Conclusions

While the example above is not the best use of recoil, I wanted to demonstrate support and a potential use case for applications using You.i Engine One. I am not advocating the use of one pattern over the other for the above, as the structure, readability, and maintainability of your code may dictate the best path forward for you.