Ex13: Shared State

In this exercise we create a dapplet with an overlay with shared state.

The overlay will be React App with Typescript (TSX).

In our dapplet we will add button with a counter and input to every tweet and to the overlay. The values of all the counters and inputs will be kept in a single shared state.

Here is the initial code for this example, which is similar to the base template: ex13-shared-state-exercise

Dapplet#

  1. Implement a type or an interface of the state with a counter and a text.

    1interface IState {
    2 counter: number
    3 text: string
    4}
  2. Use the Core.state<T> method to create a shared state. Make it at the beginning of the activate method. It has to be typed with our interface and receive the default state as a single parameter.

    const state = Core.state<IState>({ counter: 0, text: '' });
  3. Then create an IState interface type overlay.

    To share the state with the overlay add the useState method that returns the overlay itself.

    const overlay = Core.overlay<IState>({ name: 'example-13-overlay', title: 'Example 13' })
    .useState(state);
tip

In a dapplet you can create several states and overlays. So you can use one state with one or many overlays or use different states with different overlays. But note that one overlay can use only one shared state.

  1. Let's add the Core.onAction method. It inserts a home button near the dapplets name in the extension's dapplets' list. It receives a callback.

    We add a callback to the overlay opening event.

    Core.onAction(() => overlay.open());
  2. Let's pass the state's counter and text to the button's label and input's text respectively.

    We have two widgets in POST: button and input.

    1const { button, input } = this.adapter.exports;
    2this.adapter.attachConfig({
    3 POST: (ctx: any) => ([
    4 button({
    5 DEFAULT: {
    6 img: EXAMPLE_IMG,
    7 // ...
    8 },
    9 }),
    10 input({
    11 DEFAULT: {
    12 // ...
    13 },
    14 })
    15 ])
    16});

    We want to create different states for every tweet. So the keys will be the tweets' IDs.

    1{
    2 // ...
    3 label: state[ctx.id].counter,
    4}
    5// ...
    6{
    7 text: state[ctx.id].text
    8}

    You don't need to create the current context state in advance. It will be created from the default state when the key is not found in the storage.

tip

Shared state works like a key-value storage. Values are observable RxJS-based proxies.

The value of the counter is an observable object. To get the scalar value you have to use value property:

const value = state[someId].someParameter.value;

To set the new value you have to use the next method:

state[someId].someParameter.next(newValue);
  1. Increase the counter by clicking the button and open the overlay.

    1{
    2 // ...
    3 exec: () => {
    4 const oldValue = state[ctx.id].counter.value;
    5 state[ctx.id].counter.next(oldValue + 1);
    6 overlay.open(ctx.id);
    7 },
    8}

    Here we pass an optional parameter - id to the overlay.open method. Then we can get it in the overlay and use for getting and setting an appropriate part of the state.

The entire activate method:

1activate() {
2 const state = Core.state<IState>({ counter: 0, text: '' });
3 const overlay = Core.overlay<IState>({ name: 'example-13-overlay', title: 'Example 13' })
4 .useState(state);
5 Core.onAction(() => overlay.open());
6
7 const { button, input } = this.adapter.exports;
8 this.adapter.attachConfig({
9 POST: (ctx: any) => ([
10 button({
11 DEFAULT: {
12 img: EXAMPLE_IMG,
13 label: state[ctx.id].counter,
14 exec: () => {
15 state[ctx.id].counter.next(state[ctx.id].counter.value + 1);
16 overlay.open(ctx.id);
17 },
18 },
19 }),
20 input({
21 DEFAULT: {
22 text: state[ctx.id].text
23 }
24 })
25 ])
26 });
27}

Overlay#

In this example we don't talk about native JavaScript overlay because the interaction with the shared state goes through the React's HOC (Higher-Order Component). To know more about this technique check out the official documentation page.

  1. Add Share State HOC into the ./overlay/src/index.tsx

    Import dappletState from @dapplets/dapplet-overlay-bridge. This function is typed with IState interface, receives App and returns a new React component.

    1// ...
    2import App, { IState } from './App';
    3import { dappletState } from '@dapplets/dapplet-overlay-bridge';
    4
    5const DappletState = dappletState<IState>(App);
    6
    7ReactDOM.render(
    8 <React.StrictMode><DappletState/></React.StrictMode>,
    9 document.getElementById('root'),
    10);
  2. In App we paste the copied IState interface from the dapplet and export it. Then we type the module's props with IDappStateProps typed with IState.

    1// ...
    2import { IDappStateProps } from '@dapplets/dapplet-overlay-bridge';
    3
    4export interface IState {
    5 counter: any
    6 text: string
    7}
    8
    9export default class App extends React.Component<IDappStateProps<IState>> {
    10 // ...
    11}
  3. In render method get props: sharedState, changeSharedState, id

    const { sharedState, changeSharedState, id } = this.props;
  • sharedState is an object that's matched to the dapplets state but its values are scalar. So you don't need to get .value of them and you cannot change them directly.
  • changeSharedState is a function that changes the state's parametes. It receives two arguments: an object with parameters that you want to change and an ID of the changing state. The second argument is optional.
  • id is an ID, that passed through the overlay.open function.
tip

There is one key-value state that's created by default. It is state.global. Use it for the state's parameters that are common for entire app or for all IDs in the current state.

When you want to change its parameters in the overlay, you don't need to pass the second argument to the changeSharedState function.

  1. When we have an ID we need to show the counter, an input with the text and a button that increments the counter. When there is no ID (click the home button) we need to show all the states: keys with the counters' and texts' values.

    1return (
    2 <>
    3 <h1>Shared State</h1>
    4 {id ? (
    5 <>
    6 <p>Counter: {sharedState[id]?.counter ?? 0}</p>
    7 <input value={sharedState[id].text} onChange={(e) => changeSharedState?.({ text: e.target.value }, id)} />
    8 <p></p>
    9 <button className="ch-state-btn" onClick={() => changeSharedState?.({ counter: sharedState[id].counter + 1 }, id)}>Counter +1</button>
    10 </>
    11 ) : Object.entries(sharedState)
    12 .map(([id, value]: [string, any]) => <p key={id}><b>{id}:</b> {value?.counter} / {value?.text} </p>)}
    13 </>
    14);

Here is the result code of the example: ex13-shared-state-solution

Run the dapplet:

npm i
npm start