0
votes

I have a static page with two components.

  • One in the header that shows a menu handling user preferences/login and signup
  • One in the main page that is able to display a list of user images or a form about the user profile for instance.

Is there a way to share a state (let say the access_token to the API for instance) between those two components?

I could add ports to each components that would update a localStorage key and send the store to each other components.

But is there a better way for this?

1
Why not have one single big state? It’s what the Elm architecture is advocating for - Simone
Stack Overflow is not a good place to ask for opinion about "better" approaches, since it's not suited for discussion. You should instead be more specific about what your goals are, and how your current approach does not achieve those goals. - glennsl
@glennsl I would be fine with one approach - Natim
@Simone the Elm architecture gives you a state per Elm app, my question is how do you share a store between those apps. You don't have to worry about that when you build a SPA because then it is one big state which can act as a store between your pages. - Natim
the Elm architecture tries to avoid exactly this problem. Since you don’t really want to share state (because then you also need a way to keep it in sync and it gets complicated), you could simply have one app. In your case I guess that means having the two components (header and main page) rendered on the same page. - Simone

1 Answers

2
votes

The solution I ended-up with was to use two ports:

port saveStore : String -> Cmd msg

port storeChanged : (String -> msg) -> Sub msg

With decoders and encoders:

serializeStore : AccessToken -> Profile -> Cmd Msg
serializeStore access_token profile =
    encodeStore access_token profile
        |> saveStore


encodeStore : AccessToken -> Profile -> String
encodeStore access_token profile =
    let
        encoded =
            Encode.object
                [ ( "access_token", tokenToString access_token |> Encode.string )
                , ( "profile", encodeUserProfile profile )
                ]
    in
    Encode.encode 0 encoded


deserializeStore : String -> Maybe Store
deserializeStore =
    Decode.decodeString decodeStore >> Result.toMaybe


decodeStore : Decoder Store
decodeStore =
    Decode.map2 Store
        decodeToken
        (Decode.field "profile" decodeProfile)


decodeToken : Decoder AccessToken
decodeToken =
    Decode.field "access_token" Decode.string
        |> Decode.andThen
            (\token ->
                stringToToken token
                    |> Decode.succeed
            )

And then I use them to sync my component store and keep a copy on the localStorage:

  <script src="app.js"></script>
  <script>
    // The localStorage key to use to store serialized session data
    const storeKey = "store";

    const headerElement = document.getElementById("header-app");
    const headerApp = Elm.Header.init({
      node: element,
      flags: {
        rawStore: localStorage[storeKey] || ""
      }
    });

    const contentElement = document.getElementById("content-app");
    const contentApp = Elm.Content.init({
      node: element,
      flags: {
        rawStore: localStorage[storeKey] || ""
      }
    });

    headerApp.ports.saveStore.subscribe((rawStore) => {
      localStorage[storeKey] = rawStore;
      contentApp.ports.storeChanged.send(rawStore);
    });

    contentApp.ports.saveStore.subscribe((rawStore) => {
      localStorage[storeKey] = rawStore;
      headerApp.ports.storeChanged.send(rawStore);
    });

    // Ensure session is refreshed when it changes in another tab/window
    window.addEventListener("storage", (event) => {
      if (event.storageArea === localStorage && event.key === storeKey) {
        headerApp.ports.storeChanged.send(event.newValue);
        contentApp.ports.storeChanged.send(event.newValue);
      }
    }, false);
  </script>