Lev Izraelit
Lev Izraelit

Reputation: 532

Fold with initial value from localStorage in CycleJS

I am making an emoji viewer app with cycleJS, where the user can click on any emoji to add/remove it from their list of favorites. The list is also saved to localstorage on every change. I'm constructing the list using xstream by folding the click stream (either adding or removing the emoji on each click):

const favEmojis$ = clickFavEmoji$.fold(
  (favList, selectedEmoji) =>
    favList.includes(selectedEmoji)
      ? favList.filter(emoji => emoji !== selectedEmoji)
      : [...favList, selectedEmoji],
    []
);

I am able to save this stream to localStorage and load it on the page using the @cycle/storage driver:

const storageRequest$ = favEmojis$.map(favEmojis => ({
    key: "favEmojis",
    value: JSON.stringify(favEmojis)
  }));
...
return {
    DOM: vdom$,
    ...
    storage: storageRequest$
  };
}

However, I cannot figure out how to pre-load the array from localStorage into the favorite stream. Once the array is loaded from localStorage, I've tried to merge/concat it with the favEmojis$ stream in every way I could think of. For example:

const storedEmojis$ = localStorage
    .getItem("favEmojis")
    .map(favEmojis => (favEmojis ? JSON.parse(favEmojis) : []))
    .take(1);

const combinedFav$ = xs.merge(storedEmojis$, favEmojis$);

But this doesn't work - the array from localstorage gets overwritten by the folding clickFavEmoji stream. I would greatly appreciate it if someone could point me in the right direction.

N.B. the complete code is quite long, so I only included the parts that seemed the most relevant.

Upvotes: 3

Views: 77

Answers (1)

atomrc
atomrc

Reputation: 2583

The problem here is that you have two sources of truth:

  • the value used in the fold;
  • the value in the localstorage.

Both sources do not depend on each other at all, hence the weird behavior you are experiencing.

A solution that could work would be creating reducers from both you clickFav$ and storedEmojis$, merge and fold them all together.

Here is what it would look like:

const clickReducer$ = clickFavEmoji$.map(
  (favEmojis, selected) => /* same as you previous reducer */
);

const storedEmojisReducer$ = localStorage
  .getItem("favEmojis")
  .take(1)
  .map(/* serialise to json */)
  .map((state, favEmojis) => favEmojis) // here we just replace the whole state

const favEmojis$ = xs
  .merge(storedEmojisReducer$, clickReducer$)
  .fold(
    (favEmojis, reducer) => reducer(favEmojis)
  , [])

return {
  DOM: favEmojis$.map(render)
}

This way, there is an explicit relation between the value in the localStorage and the value that evolves over the application's life cycle.

With onionify

Now, the previous solution works well. When the reducer is called, it is aware of the previous value given by the localStorage. But if you look closer at the code that creates the favEmojis$, it is pretty much noise. It has no specific business logic, it is just dumbly calling the given reducers.

onionify (https://github.com/staltz/cycle-onionify) greatly simplifies the process of managing state in a cycle app by centralizing all the calls to the reducers in a single point and re-injecting the new state into your application's sources.

The code won't change much from the previous version, the changes would be : - the state will be injected as an explicit dependency of your component ; - you won't have to manually call the reducers.

function Component({ DOM, onion /* ... */ }) {
  const clickReducer$ = /* same as before */

  const storedEmojisReducer$ = /* same as before */

  return {
    DOM: onion
      .state$ // the state is now inside onionify
      .map(render),

    // send the reducers to onionify
    onion: xs.merge(storedEmojisReducer$, clickReducer$)
  }
}

Upvotes: 2

Related Questions