Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to create an Elm like hook? #20

Closed
nojaf opened this issue Oct 6, 2022 · 13 comments
Closed

How to create an Elm like hook? #20

nojaf opened this issue Oct 6, 2022 · 13 comments
Labels
documentation Improvements or additions to documentation
Milestone

Comments

@nojaf
Copy link

nojaf commented Oct 6, 2022

Hello, I'm wondering how I could leverage this project to create an Elm style (or reducerlike) hook.

Something along the lines of:

import { useObservable, enableLegendStateReact } from "@legendapp/state/react";
import { useRef } from "react";

enableLegendStateReact();

const useLegendaryHook = (init, update) => {
  const model = useObservable(init());

  const dispatch = (msg: any) => {
    const nextModel = update(msg, model.get());
    model.set(nextModel);
  };

  return [model.get(), dispatch];
};

const init = () => 0;
const update = (msg, model) => {
  if (msg && msg.type === "increase") {
    return model + 1;
  } else {
    return model;
  }
};

export default function App() {
  const renderCount = ++useRef(0).current;

  const [model, dispatch] = useLegendaryHook(init, update);

  // Text element re-renders itself
  return (
    <>
      <div>Renders: {renderCount}</div>
      <div>Count: {model}</div>
      <button onClick={() => dispatch({ type: "increase" })}>click</button>
    </>
  );
}

The model type in this simple example is just a number and my reducer function update takes in a message and creates a new model based on the received msg.

In this example, I get two new renders for each click, so I'm most likely missing the point somewhere.

Any thoughts on this?

@jmeistrich
Copy link
Contributor

jmeistrich commented Oct 7, 2022

The reason it's rendering twice is because of the get() in the hook, which returns the raw value and subscribes the component to updates. If you change that to

return [model, dispatch];

then the hook would return an observable without subscribing to updates. And then rendering the observable directly in <div>Count: {model}</div> would create a text element that re-renders itself.

As for the concept, it looks like it would work. But it is a lot of extra code. I'm curious what the overall goal is - maybe we could come up with something easier/shorter that could achieve the same goal?

@nojaf
Copy link
Author

nojaf commented Oct 7, 2022

That makes sense, thanks for explaining!

I guess, I want something that comes close to useReducer.

@jmeistrich
Copy link
Contributor

jmeistrich commented Oct 7, 2022

I added a useObservableReducer in 0.19.4, similar to what you had but trying to support all of React's normal overloads. It basically looks like this:

export function useObservableReducer(reducer, initializerArg, initializer) {
    const obs = useObservable(initializerArg !== undefined ? initializer(initializerArg) : initializerArg);
    const dispatch = (action) => {
        obs.set(reducer(obs.get(), action));
    };
    return [obs, dispatch];
}

Does it seem correct to you? I don't use reducers so I'm not 100% sure of all the ways to use them.

@nojaf
Copy link
Author

nojaf commented Oct 9, 2022

Many thanks!

@attiqeurrehman
Copy link

@jmeistrich any documentation for it?

@jmeistrich
Copy link
Contributor

@attiqeurrehman Oh I forgot to add that to the docs! I'll add that to my todo list and reopen this issue until it is documented.

But it should be pretty straightforward in theory - use it as you would use useReducer but it returns an observable instead of a raw value as the first element of the array. Are you finding it to be doing something strange?

@jmeistrich jmeistrich reopened this Oct 25, 2022
@jmeistrich jmeistrich added the documentation Improvements or additions to documentation label Oct 26, 2022
@attiqeurrehman
Copy link

@jmeistrich thanks for the quick reply. I am having trouble with reducer actions:

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

how it will look with the observable?

@jmeistrich
Copy link
Contributor

You should be able to just replace the useReducer call like this:

const [observableTasks, dispatch] = useObservableReducer(tasksReducer, initialTasks);
const tasks = observableTasks.get()

Is that not working? If not, if you can post a codesandbox or a full example with expected results for me to test with, that would be very helpful to track down the problem!

@attiqeurrehman
Copy link

Here is the codesandbox but I am getting the following error:

initializer is not a function

@jmeistrich
Copy link
Contributor

@attiqeurrehman Got it, thanks. I'll fix that soon and let you know.

@attiqeurrehman
Copy link

sounds good, looking forward to it.

@jmeistrich
Copy link
Contributor

Fixed in 0.21.4. But just one other thing on your example - the component needs to be an observer to re-render itself when the value of the observable changes. I made that change and updated the version in package.json in https://codesandbox.io/s/dreamy-estrela-letwfi.

@attiqeurrehman
Copy link

@jmeistrich that was fast and awesome!

@jmeistrich jmeistrich added this to the 1.0 Release milestone Nov 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants