Giter VIP home page Giter VIP logo

overmind's Introduction

Cerebral

A declarative state and side effects management solution for popular JavaScript frameworks

NPM version Build status Coverage Status bitHound Score Commitizen friendly Discord

Maintainer needed

https://gist.github.com/christianalfoni/f1c4bfe320dcb24c403635d9bca3fa40

Documentation

Contribute

The entire Cerebral codebase has been rewritten to encourage contributions. The code is cleaned up, commented and all code is in a "monorepo". That means you can run tests across projects and general management of the code is simplified a lot.

  1. Clone the monorepo: git clone https://github.com/cerebral/cerebral.git
  2. In root: npm install

The packages are located under packages folder and there is no need to run npm install for each package.

Using monorepo for your own apps

If you want to use Cerebral 2 directly from your cloned repo, you can create a symlinks for following directories into the node_modules directory of your app:

  • packages/node_modules/cerebral
  • packages/node_modules/function-tree
  • packages/node_modules/@cerebral

If your app and the cerebral monorepo are in the same folder you can do from inside your app directory:

$ ln -s ../../cerebral/packages/node_modules/cerebral/ node_modules/
# ...

Just remember to unlink the package before installing it from npm:

$ unlink node_modules/cerebral
# ...

Running demos

Go to the respective packages/demos/some-demo-folder and run npm start

Testing

You can run all tests in all packages from root:

npm test

Or you can run tests for specific packages by going to package root and do the same:

npm test

Changing the code

When you make a code change you should create a branch first. When the code is changed and backed up by a test you can commit it from the root using:

npm run commit

This will give you a guide to creating a commit message. Then you just push and create a pull request as normal on Github.

Release process

  • Review and merge PRs into next branch. It is safe to use "Update branch", the commit created by Github will not be part of next history
  • If changes to repo-cooker, clean Travis NPM cache
  • From command line:
$ git checkout next
$ git pull
$ npm install # make sure any new dependencies are installed
$ npm install --no-save repo-cooker # needed to test release, make sure you have latest
$ npm run release # and check release notes
$ git checkout master
$ git pull
$ git merge --ff-only next
$ git push

overmind's People

Contributors

abalmos avatar christianalfoni avatar corlaez avatar crimson-med avatar danielnaab avatar dependabot[bot] avatar dpraimeyuu avatar edgesoft avatar eirikhm avatar etienne-dldc avatar fopsdev avatar fweinb avatar garth avatar gaspard avatar geirsagberg avatar grandevel avatar guria avatar henri-hulski avatar hipertracker avatar jhsu avatar luisherranz avatar menberg avatar mfeitoza avatar mi-kas avatar mieky avatar oakgary avatar reflog avatar schabluk avatar schiller-manuel avatar yesmeck avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

overmind's Issues

multiple trackings on same component does not work

hi there

i have some custom-elments tracking state changes.
but the render() implementation is not at a single point.
this means that in my case there get some additional rendering called after the main render.
(i'm using lit-html and there is a repeat directive and which gets executed after the main rendering)
so i have to track those as well.
now i've discovered that a subsequent start and end-tracking on a component followed by a mutationlistener.update will result in loosing the paths from the first tracking

https://codesandbox.io/s/1ym754x404

any help appreciated

parallel operator crashes in production build

Hi,

I use parallel to load two resources at the same time - works fine in dev. When I run the dist files, I get "Cannot read property 'concat' of undefined", which points here:

context.execution.path.concat(String(index))

export const loadFirst: Action = async ({ state, effects }) => {
  state.first = await effects.api.getFirst()
}

export const loadSecond: Action = async ({ state, effects }) => {
  state.second = await effects.api.getSecond()
}

export const loadAll: Operator = parallel(
  action(loadFirst),
  action(loadSecond)
)

I'm using it in a React project created with create-react-app.

Vue code smells

The Vue code examples look like they were the first time someone ever used Vue. Here are some tips to clean up code smells:

  • In the Vue world you have things like v-on:click="doThing" and v-bind:title="dataValue" and then you have the shorthand versions of these, @click="doThing" and :title="dataValue". And litterally everyone just uses the shorthand. You are using a mix of the two, just use the shorthand.
  • You are importing connect in every Vue component and then wrapping the export default in the connect. Gross. Don't do that. Follow the pattern that Vuex (and other global things like Vue-i18n, Vue-Router, etc) uses. In the overmind.js
    import Vue from 'vue';
    import { Overmind } from "overmind";
    import { createConnect } from "overmind-vue";
    Vue.use(Overmind);
    const overmind = new Overmind({
      state: {
        isLoadingPosts: true
      }
    });
    export const connect = createConnect(overmind);
    in main.js
    import Vue from 'vue';
    import store from './overmind.js';
    const app = new Vue({
      store
      // ...
    });
    Or something similar to that pattern.
  • In the template section put a space inside double curly braces. {{ todo.title }}
  • Feels weird to have to baby sit Overmind by manually adding and removing mutation listeners.
  • Not seeing any dev tools examples. Vuex is built in to Vue devtools and is very handy for inspecting the current state or even modifying the state directly.

Devtools class

class Devtools {
  state
  connect(host) {}
}

This version takes in the current state of the application and when connected to the devtools app it will pass that initial state.

Logic for managing the initial "ping" / "pong" with the devtools app is also included in this implementation.

Combining different stores

Hi, I would like to know if this possible or can be implemented in the future to use different store value in the derived value?

pseudocode:

const users = createOvermind({
  state: {
    1: {
      id: 1,
      name: "alex"
    },
  }
});

const posts = createOvermind({
  state: {
    list: [{ id: 1, authorId: 1, text: "..." }]
  }
});

const postsWithAuthor = combine(users, posts, (_users, _posts) => {
  return _posts.list.map(post => {
    return {
      ...post,
      author: _users[post.authorId]
    };
  });
});

Bind this as context in actions and effects

It would be great if we could inject the context as this in actions and effects. I tried but I did not manage to type effects this way because editor.fooEffect() sets this for typescript (because TS does not know that we wrap the call). Writing the code to bind this as context is easy but I did not manage to make TS happy with the following code:

Experiment with this bound to context:

export const createContact: Operator = action(function (type) { // no more arrow here
  const { data, dialog } = this.effects // this works

  data.createContact(type)
  data.clearForm()
  dialog.show('ContactPrint')
})

For this idea to work, we need to remove this argument in effects:

interface Foo {
  (this: number, value: string): boolean
}

interface Bar {
  (value: string): boolean
}

type RemoveThis<T> = T extends (this: any, v: infer V) => infer U ? (value: V) => U : never

type Foo2 = RemoveThis<Foo> // (value: string) => boolean
type Bar2 = RemoveThis<Bar>// (value: string) => boolean

This solution would be really great because the user would not have to care about context at all anymore.

Summary

There are two propositions here:

  1. Move value outside of the action context to clarify api (stable part is context, changing part is value).
  2. Use this as context in actions and effects.

Async actions don't return a proper promise

I'm wondering why async actions don't return a proper promise:

actions: {
    waitFor3sec: async () => {
      await new Promise(resolve => setTimeout(resolve, 3000));
      return "done";
    }
  }

// ...
console.log("now");
await overmind.actions.waitFor3sec(); // <- this seems to be sync
console.log("after 3 seconds");

Complete example here: https://codesandbox.io/s/6loqylpk3k

They also don't seem to return the returned value ('done' in this case).

We are currently using MobxStateTree but thinking about moving to Overmind.

State doesn't change for operators in a pipe

Consider we have the following pipe:

pipe(
  operatorA,
  operatorB,
)

Now imagine operatorA changes state.someProp and operatorB reads state.someProp.
Currently it seems that operatorB gets the original, unchanged state.

Question is: is this the expected behavior? Is there a way to have the operators receive the most recent state? Or maybe my approach is incorrect.

to track or not to track

hi guys

i came across a use case where pausing and resuming the tracking of state paths would be beneficial.

usecase

imagine a table with multiple rows which supports the following features (simplified for now):

  • sorting
  • inplace editing of the cells

now imagine we have the following table sorted by column Name:

Id    Name
1     Anthony
2     Beat

lets say our state that holds the rows looks like:

myRows:
{
1:{Id:1,Name:"Anthony"},
2: {Id:2, Name:"Beat"
}

after rendering the tracked paths will contain:

myRows.1
myRows.1.Name
myRows.2
myRows.2.Name

(because the sorting method accessed those paths)

now inplace editing the table to:

Id    Name
1     Christian
2     Beat

will result in the table to be immediately flipped over to:

Id    Name
2     Beat
1     Christian

because of the sorting

This is not a good user experience. What i would like is that the sorting just happens after explicitly clicking on the column header and not right after editing a cell.

if i now could something like

app.pauseTracking()
dotheSortingThingy()
app.resumeTracking()

those paths wouldn't be tracked and my use case would be fine :)

Unnecessary mutations

I'm studying Overmind and everything seems so carefully planned that one thing surprised me: mutating the same part of the state with the same value does trigger a new mutation.

I'll show it with an example:

// store.js
const overmind = new Overmind({
  onInitialize: ({ value }) => {
    value.addMutationListener(mutation => console.log(mutation));
  },
  state: {
    name: 'John'
  },
  actions: {
    changeName: ({ state }) => {
      state.name = 'Peter';
    }
  }
});

// Name.js
function Name() {
  const { state } = useOvermind();
  console.log('<Name /> rendered');
  return <h1>Hello {state.name}</h1>;
}

// App.js
function App() {
  const { actions } = useOvermind();
  return (
    <div className="App">
      <Name />
      <button onClick={actions.changeName}>Change name to Peter</button>
    </div>
  );
}

Clicking on the <button> triggers a new mutation and a new (unnecessary) re-render of the <Name /> component.

Logs are:

Object {method: "set", path: "name", args: ["Peter"]}
<Name /> rendered
Object {method: "set", path: "name", args: ["Peter"]}
<Name /> rendered
Object {method: "set", path: "name", args: ["Peter"]}
<Name /> rendered
...

codesandbox: https://codesandbox.io/s/7wjj165ynq

I saw the same behaviour in ProxyStateTree: https://codesandbox.io/s/3846ww49qq (I modified the toggleItemCompleted action)

I'm used to Mobx, which doesn't trigger mutations if the value doesn't change, not matter what you do.

How's this going to work in Overmind/ProxyStateTree? Have you already thought about the overhead of comparing those scalar values vs the performance gains derived from avoiding those unnecessary re-renders?

Separate value from context in actions

Now that we have async mutations and effects tracking, I think we can use these new features to compose actions in pure javascript, without arrays or other syntax complications.

In order to improve the experience with this type of composition, I propose to move the value outside of the action context because it is different (as it changes) and makes it easier to tweak the value before passing everything to the next function call.

Example:

export const backspacePress: Operator<CompositionSelectionArgs> = action(
  (ctx, value) => {
    const { editor } = ctx.effects

    // Action composition as pure JS or TypeScript
    editor.ensureComposition(ctx, value.holder)
    const ops = editor.deleteSelection(ctx, value)
    editor.processOps(ctx, ops)
    editor.changed(ctx, value.holder)
  }
)

Other example (composition with operations from multiple libraries):

export const createContact: Operator = action((ctx, type) => {
  const { data, dialog } = ctx.effects

  data.createContact(ctx, type)
  data.clearForm(ctx)
  dialog.show(ctx, 'ContactPrint')
})

Namespacing limitations or approach

New to overmind, but namespacing seems to be causing me problems. Is it desirable to keep things as denormalized as possible and avoid namespacing alltogether?

  1. One thing I run into is, you can't Derive<> from another namespace.

  2. Second is, if you want to send state from one namespace to another with an action (just copy one object to the other, you end up with proxy duplication errors.

Am I wrong on 1? And is 2 possible without cloning objects?

Code sharing with mobx

Just curious, we use immer/mobx. Is this / will this use the same low level libraries? Just curious overhead of adding this as well.

Make `derive` a special kind of proxy to not resolve all functions

The gist of the idea is to change dynamic values resolution from resolving all functions to using a dedicated wrapper.

This is a breaking change but it fixes the most annoying and illogical feature of overmind. This feature has a big impact on many parts of Overmind without the added value being enough of a justification for this in my view.

Pros

So first I'll start with the nice value of this functions are called on get. Creating a derived value is elegant and simple: simply insert a function in the state tree. Getting the value back is like getting any other value, without any extra work.

Cons

My biggest grudge against this is that it breaks one of the most intuitive aspects of a state tree: read & write consistency. This "feature" breaks types and intuition all the time:

// Config definition, here `sortedKeys` is a function.
interface MyState {
  sortedKeys: (...) => {...} // derived value
  data: ...
}

// In the context, here `sortedKeys` is an array of strings:
ctx.state.sortedKeys

// When writing to state, if you change the sorted functions, you have to do:
ctx.state.sortedKeys = (...) => {....}
// and when you read value back
const func = ctx.state.sortedKeys
// you get... the result of the executed function. Totally unintuitive and dangerously messy

In some earlier version of Overmind, only the functions set on initial state definition would behave like this and at least, it made the write/read in the app consistent. But now, this is really not cool at all because you are forced to break your own types to change a function or are forced to wrap the function in a function to avoid execution on read.

This has multiple consequences:

  1. write / read in the state is inconsitent
  2. it is easy to put a function in the state and have weird bugs with the function being executed in surprising ways
  3. types resolution (ResolveState) is unnecessarily complicated
  4. it makes storing components or other "complicated values" behind functions ugly

And finally, the most important fact:

  1. It is super easy to fix all these annoying problems by simply wrapping the derived state in a derive function that simply attaches a symbol to the passed function: IS_DERIVE.
ctx.state.sortedKeys = derive((...) => {...}) // this is the only required change: use the `derive` function on write. We can even trick TS into thinking we are writing and array of strings if we want so that this operation is type safe and consistent.

Concerning implementation, we simply check someFunc[IS_DERIVE] before executing the function on get. We can use a simple trick to resolve state type based on using the Derive<T> thing.

Or (my favorite solution) just remove everything related to derive and simply let people use effects:

// Using effects
const sortedKeys = ctx.effects.sortedKeys(data)

// Or using any function
const sortedKeys = sortKeys(data)

Since derive is not cached and does not have anything special about it, the solution above works just fine and removes quite some complexity in the whole state handling thing.

Proposal: Operators to track state mutations

The problem: tracking state mutations

I really like Flux but in my experience, it has one flaw: sometimes you want to trigger a side-effect when some part of the state mutates. In Flux, everything has to be triggered by an action so you need to listen to all the actions that may mutate that state.

For example, in redux-saga, if you want to send an analytics event when state.currentPage changes you need to know all the actions that modify state.currentPage:

function* currentPageChanged() {
  yield takeEvery([
    "HOME_PAGE",
    "ABOUT_PAGE",
    "DOCS_PAGE",
    "POST_PAGE",
    "LIST_OF_POSTS_PAGE",
    ...
  ], sendAnalyticsEvent)
}

Sometimes you can save the day consolidating all the actions in a single one: CHANGE_PAGE, but sometimes you just can't, the actions are too different or doesn't make sense to join them.

This pattern quickly leads to code that is difficult to maintain and prone to bugs: in this case, developers need to remember to change the analytics saga each time they add/remove a new route. This may seem not so important at first, but when the team is big or the app is complex, figuring out why things are broken is not trivial.

In the end, developers are forced to maintain a list of actions that modify some part of state when all they really want is to listen that state mutation. This is a bad developer experience and leads to boilerplate code and bugs.

if (state.currentPage !== previousState.currentPage)
  sendAnalyticsEvent()

There's a workaround in the redux/redux-saga and although it is in theory forbiden, it works surprisingly well:

let previousCurrentPage
function* currentPageChanged() {
  while(true)
    yield take("*")
    const state = yield select()
    if (previousCurrentPage !== state.currentPage) {
      previousCurrentPage = state.currentPage
      sendAnalyticsEvent()
    }
  }
}

In Mobx/MobxStateTree it is simpler because you have reactions and autoruns precisely for that:

reaction(() => state.currentPage, sendAnalyticsEvent)

Proposal for Overmind

Right now in Overmind you can use addMutationListener in the onInitialize action, but I don't think it's a good API for this. I've been taking a look at the operators and I think there's a more elegant way to solve this.

So here is my proposal:

The waitForMutation operator

This operator stops the pipe until the state mutates. This is similar to Mobx's reaction.

pipe(
  waitForMutation(({ state }) => state.currentPage),
  sendAnalyticsEvent
)

The waitUntilTrue operator

This operator stops the pipe until the function returns true. This is similar to Mobx's when.

pipe(
  waitUntilTrue(({ state }) => state.currentPage !== '/'),
  sendAnalyticsEvent
)

Implementation idea

I've been taking a look the Create custom operators guide and these operators could be created with a new createTrackOperator where context.state uses a getTrackStateTree. I guess it would look like something like this:

export function waitForMutation(operation) {
  return createOperator(
    'waitForMutation',
    operation.name,
    (err, context, value, next) => {
      if (err) next(err, value)
      else {
        const tree = context.getTrackStateTree()
        operation({ ...context, state: tree.state }, value)
        tree.track(() => next(null, value))
      }
    }
  )
}

Other useful operators

The startOver operator

When reached, this operator starts again the pipe from the top.

pipe(
  waitForMutation(({ state }) => state.currentPage),
  sendAnalyticsEvent,
  startOver
)

The race operator

This has nothing to do with the issue topic but I thought it'd be cool to have a race operator with the combination of startOver for timetous and things like that :)

If parallel is like Promise.all, race is like Promise.race:

pipe(
  race({
    succeed: fetchSomething,
    failed: wait(5000)
  }),
  when(_, ({ succeed }) => !!succeed, {
    true: doSomethingElse,
    false: startOver  // <- try it again
  })
)

By the way @christianalfoni, I saw you also used this type of reactions in your codesandbox refactoring:
https://github.com/cerebral/codesandbox-client/blob/sandbox/packages/app/src/app/pages/Sandbox/Editor/Content/index.js#L36

So I hope we are in the same boat here :)

Example apps / todomvc

I've looked around on github and codesandbox but haven't found any projects demonstrating overmind in more complex applications - just a lot of "Overmind demo 1" clones.

There used to be a demos package 606f1a6 in this repo but it was removed nov 13 2018 9ecab06

  1. it would be nice if there were a list that gathered examples as they are developed. I'll use the comments to this issue to do so for now.

  2. As a first step, I adapted the overmind routing tutorial to a react js app (and found a bug in the last tutorial step as I did so #219)

  3. There are a couple of TodoMVC apps using overmind on codesandbox, but all the ones I checked are broken due to breaking changes in later overmind versions.

  4. therefore, since overmind is still changing, I guess its more important to have a few good example apps that are consistently maintained rather than a big list of outdated examples. I would be happy to help with one of these - maybe todomvc?


overmind example apps

derive not returning correct derived state

Following along with the youtube raw preview and the guide on the website (react/typescript), I am fetching data in componentDidMount that then sets posts (this.props.app.actions.loadPosts()). When I add in derived state for unreadCount, the correct count appears only after I click a post to toggle it read.

I apologize if I am misunderstanding something.
Thank you for your work. :)

Support for React Concurrency/Hooks

As I understand, React is going with a pull rather than push based model. I could imagine overmind working with that by (in the abstract) storing the "previous" or current value in a cache until the next update propagates.

Just wanted to open the ticket here so there's a googleable discussion: would this be possible and coherent with Concurrent mode? Is it supported yet?

Thoughts on Context

@christianalfoni Just iterating on context type.

First I think it could make sense to use the notion of Context for the operation argument like we used in Cerebral. So no more effects.

type BaseContext<App> = App['context'] & { state: App['state'] }
type Context<App, Value> = BaseContext<App> & { value: Value }

And we get back what we had:

// Manual Config definition
export interface Config {
  state: {
    foo: string
  },
  context: { // instead of `effects`
    http: {...},
  }
}
export type App = TApp<Config>
export type Action = TAction<App>

Action definition:

const foo: Action<string> = action =>
  action
    .map(({ http }) => http.get(...))
    .mutate(({ state, value }) => (state.foo = value))

We just have to be careful as to how we construct the App type (by hand or by inspection of the config) to avoid context = any.

I also think it would make a lot of things simpler in overmind types if we passed BaseContext around instead of Effects and State.

Module class

import Overmind from 'overmind'

const app = new Overmind({
  reactions: {},
  state: {},
  actions: {},
  catch: [],
  modules: {}
})

The first argument to the main class constructor is the "main module definition". This definition is passed into a Module class:

class Module {
  constructor (moduleDefinition) {}
}

The Module class is responsible for traversing the definition and do the following conversions:

  1. Make actions runable by wrapping them in a function which uses the runAction method
  2. Replace sub module definitions with Module classes receiving those definitions

Breaking changes before official release

After the announcement we have gotten a lot more people testing out Overmind. Especially the Vue and Angular (coming soon) views has gotten some amazing updates. But also smaller details on the API can be improved. In this issue we will list the breaking changes we want to make before the official release:

1. Separate value from configuration

In actions you receive:

const myAction = ({ state, effects, actions, value })  => {

}

This value does not really belong in the rest which is defined as "the configuration" of the app, it maps directly to the configuration. So we want to separate that out:

const myAction = ({ state, effects, action }, value)  => {

}

This is more intuitive, as you can think of the first argument as "the context" of the action, pointing to the Overmind instance.

Amount of work

  • Refactor devtools
  • Refactor website
  • Rewrite all documentation (there are a lot of examples)
  • The VueX video is wrong... though we can maybe add a note to it instead. I have not figured out how to add like "notes"... maybe we can use this link notifier and say: "API CHANGE" and link to the documentation... yeah, that could work

RFC: Replace run and mutate operator with action

So we are closing in on release, woop woop!

I have one final breaking change I want to do. I want to replace the run and mutate operator with a single action operator. Let me explain why.

1. Introducing the concept

When we now explain Overmind we start out with simple imperative actions.

const myAction = ({ state, value }) => {
  state.foo = value
}

When we move to the functional world of operators we could make the jump by simply doing:

const myAction = action(({ state, value }) => {
  state.foo = value
})

Congrats, you have now created a lego block out of your action. You can now make this action only callable when the value has a certain length:

const myAction = pipe(
  filter(({ value }) => value.length > 2),
  action(({ state, value }) => {
    state.foo = value
  })
)

And so on. It creates a natural stepping stone from imperative actions to functional actions, where you have additional operators to manage "FLOW".

2. Overdoing the separation

The operators API should be closer to an action than rxjs. The reason is that there are only certain functional aspects that truly gives value. Let me explain with an example:

export const search = pipe(
  setQuery,
  filterValidQuery,
  debounce(200),
  getResult,
  catchResultError,
  setResult
)

This looks very nice, but the last part there. Creating three composable pieces to get and set the search result. This is where I think we are overdoing it. I think it makes a lot more sense to do:

export const search = pipe(
  setQuery,
  filterValidQuery,
  debounce(200),
  getSearchResult
)

Where getSearchResult looks like:

export const getSearchResult = action(async ({ state, api, value: query ) => {
  try {
    state.searchResult = await api.getPosts(query)
  } catch (error) {
    state.searchError = error
  }
})

Summary

So by getting rid of run and mutate we reduce API surface, we create a simpler transition to the functional world and we expose operators that actually manages FLOW. Like filter, debounce etc. Now map becomes an operator to put values into the flow, not as an intermediate step every time you want to express "grab data from effect and set state"... which is what we mostly do :)

Okay... thanks for listening :-)

Support for IE11

Currently, we have a couple of products in various frameworks that need to support ie11. We would like to port some of those projects to React.

Overmind is very appealing because of its simplicity and the fact that it is framework agnostic, but not supporting ie11 is a hard requirement to overcome.

My suggestion would be to look into using immer or even replicating what it does. Like overmind, immer uses proxies, but there is an ES5 compatible implementation to support older JavaScript environments. This should allow for targeting older browsers with a very similar API at a relatively small cost (4kb)

Offline examples

It would be nice to have an example of how to implement redux-offline features using overmind.

Main class

import { App } from 'overmind'

const { state } = new App({
  state: {}
}, {
  devtools: 'localhost:8585'
})

The main class is responsible for the following:

  • Create an instance of proxy-state-tree and pass in the state
  • Create an instance of Devtools class with the initial state and connect

Summary of Typescript bug

Okay, so we had an issue with Typescript. It seems that the following behaves differently in Typescript:

Works

import * as actions from "./actions"
import * as state from "./state"

export { actions, state }

Works

import * as actionsA from "./actionsA"
import * as actionsB from "./actionsB"
import * as state from "./state"

const actions = Object.assign({}, actionsA, actionsB)

export { actions, state }

Does NOT work

import * as actionsA from "./actionsA"
import * as actionsB from "./actionsB"
import * as state from "./state"

const actions = { ...actionsA, ...actionsB }

export { actions, state }

Does NOT work

import { someAction } from "./actions"
import * as state from "./state"

const actions = { someAction }

export { actions, state }

To me it seems that creating a new object here breaks the typing. As if the import * as something is not really a traditional object, it is an "exports object". If you start breaking that object apart the typing gets lost. The weird thing is that Object.assign still works, but that could because of its explicit typing.

Anyways... this is the issue and if someone wants to throw their thoughts on this maybe we could create a Typescript bug issue.

Take a turn

I have been playing with this "pipe" API and it led me to think about something. What if Overmind just provides the "context" to the action?

export const getUser: Action<string> = async ({ value : id, state, effects }) => {
  state.isLoadingUser = true
  const user = await effects.api.getUser(id)
  state.users.push(user)
  state.isLoadingUser = false
}

Every time an action is called with a value Overmind intercepts and produces a context with that value, adding the state and effects. This context can now be used directly. Since we have a concept of effects we can also track async mutations. (which mobx state tree has an issue doing).

When we got this running we can open up for any declarative functional abstraction:

Current chaining API:

export const getUser: Action<string> = (context ) =>
  action(context)
    .map()
    .filter()
    .mutate()

A piping api:

export const getUser: Action<string> = (context ) =>
  pipe(
    context,
    getUser,
    setUser
  )

I think it makes sense to create a low level concept of context which can be used directly. That way when getting into Overmind you do not have to go functional. Some apps does not require it at all. But you still get the debugging experience etc. In addition we are not locking out other existing flow control solutions. You just have to provide it the context.

Then you can move functional in the actions you want to or do it all over.... or not at all.

HMR causing freeze/infinite loop

So, not sure what's causing this. I'm basically just testing Overmind in our app, I put together the router and some small state and hooked it into the app.

For some reason at some point, HMR started doing a very heavy infinite loop that will freeze the tab quickly. I added some logpoints and a breakpoint on the HMR condition, you can see the behavior here:

https://v.usetapes.com/BUENSeCBBt

Any ideas what would cause this? It seems to be calling itself somehow at the forceUpdate area and just continuing to do so ever after.

Hot-reload issue

Hey, thank you for the awesome lib. I'm really enjoying it so far. However I'm experiencing not critical, but a bit annoying thing. When my app is hot reloaded, components refetch data from the server and this data is written to the state. But this exact data is already in the state and thus I get this error:

Error: proxy-state-tree - You are trying to insert a value that already exists in the state tree on path

So the question is: do I need to handle it in any way? Is there a way to get rid of this error? Or should I just forget about it, because it's only relevant for development mode?

find another name for `map` operation

Hi everyone, if you have an opinion on changing the name of the map operation, please add a comment with your proposition and people will vote after some discussion using the thumbs up reaction on the comment :-)

Example request: Undo/Redo history

I saw the new dev tools shows this, just think it would be helpful to see an idea of how you'd implement this.

Being able to choose the granularity of undo/redo would be nice. Ideally by marking a point as a checkpoint.

Bind this as context in actions and effects

We can inject the context as this in actions and effects to simplify actions and effects even more. This creates a very intuitive and clean api where there is no confusion as to argument order (context, value) or (value, context) and where helpers that do not require any context are just simple functions (without the need for an empty context slot):

Example:

export const createContact: Operator = action(function (type) { // no more arrow here
  const { data, dialog } = this.effects

  data.createContact(type)
  data.clearForm()
  dialog.show('ContactPrint')
})

For this idea to work, we need to remove this argument in effects:

interface Foo {
  (this: number, value: string): boolean
}

interface Bar {
  (value: string): boolean
}

type RemoveThis<T> = T extends (this: any, v: infer V) => infer U ? (value: V) => U : never

type Foo2 = RemoveThis<Foo> // (value: string) => boolean
type Bar2 = RemoveThis<Bar>// (value: string) => boolean

With this solution, the user has an added advantage to using effects (on top of devtools) because he/she does not have to care about passing the context around at all anymore.

allow Functions in State Tree

hi there

in old cerebral i was putting functions directly on the state tree.
a usecase:
imagine a table component which can be used just by handing over a piece of state in a given structure.
eg:

    Fields: {
      IDTransaction: {
        pos: 0,
        nameFn: device => (device.IsMobile ? "ID" : "ID Transaction"),
        valueFn: val => val,
        displayFn: val => val,
        editable: false,
        visible: true,
        width: 10
      },

nameFn:
since tables are a bit a no-go for displaying on mobile but still required for certain LOB-type applications this function dynamically adjusts the title of the column depending on the device (to save some space)
aother one:

...
 displayFn: val => defaultDateFormatter.format(new Date(val)),
...

displayFn: used to format the output in each cell (a very hot path therefore and thats why i hesitate a bit to use computeds for this "fire and forget" - values)

with this setup one is able to define a custom table component only in state => the definition is kept together and easy to maintain. in this case i hand the object over to a custom element like <table-h .obj=${{TableDef:state.TestTable}} /> and thats all thats needed to render a table.

what are my options:

  • use computeds (but those cell values are not reused often, maybe for dates it would make sense depending on the project)
  • use a datatype so the component already knows how to render this
    -> this i will do but still there are edge cases where i would like to have more control
  • handing over a options object as well, smth like this:
    const obj = {TableDef: state.TestTable, Options: {ColumnOptions: {IDTransaction: {valueFn: ...}}
    but then i have to duplicate the columndef which i already have nicely in the state...
  • a new function type in overmind which is just for executing functions and not using a cache
    -> well i could maybe use compute(...limit:0) or similar but its really verbose for this usecase
  • change proxy-state-tree to allow simple functions in state tree. here is an attempt: #75

So therefore hopefully you understand the motivation behind this request. would appreciate to hear your thoughts on this.

When using a map all state needs to be in place to not re-render the whole app

Consider this state

export const itemCategories = {
  '1': {
    name: 'Some 1',
    id: '1'
  },
  '2': {
    name: 'Some 2',
    id: '2'
  },
  '3': {
    name: 'Some 3',
    id: '3'
  }
}

If I want to change something in the map that is undefined like isOpened I will get a full re-render of the first component in the tree. Expected, but console.log in Object.Keys happens for all items. That means App is also re-rendered

App and ItemCategory

import React from 'react'
import 'index.css'
import { connect } from 'app'
import { ItemCategory } from 'components/ItemCategory'

const App = ({ app }) => {
  return (
    <div style={{ height: '100%' }}>
      {Object.keys(app.state.itemCategories).map((key, index) => {
        const itemCategory = app.state.itemCategories[key]
        console.log(key)
        return <ItemCategory key={index} itemCategory={itemCategory} />
      })}
    </div>
  )
}

export default connect(App)
const ItemCategory = ({ app, itemCategory }) => {
  console.log('render', itemCategory.id)
  return (
    <Container
      isOpened={itemCategory.isOpened}
      onClick={e => {
        app.actions.toggleItemCategory({ key: itemCategory.id })
      }}>
      {String(itemCategory.isOpened)}
    </Container>
  )
}

export default connect(ItemCategory)

If we change the state to have isOpened everything works as expected. We do not get a re-render i App.

export const itemCategories = {
  '1': {
    name: 'Some 1',
    id: '1',
    isOpened: false
  },
  '2': {
    name: 'Some 2',
    id: '2',
    isOpened: false
  },
  '3': {
    name: 'Some 3',
    id: '3',
    isOpened: false
  }
}

Actions not passed to connected component on server.

I am not sure if this is done by design, but having the app setup as instructed in the structuring your app and server rendering sections of the guide my app on the server breaks. I am getting and error cannot real modals of undefined error when trying to access actions.modals.openModal in an onClick handler.

const { actions } = useOvermind();
In render

return (
  <div  onClick={actions.sidebars.toggleDrawer}></div>
)

I could set actions to a default {} but wanted to make sure I wasn't doing anything wrong.

Huge fan of this library coming from Baobab and Celebral! Great Work!

Proposal: rename value to payload

I've been following the conversation here #222 about the possibility of having an arbitrary number of arguments and it looks like only one argument will be allowed.

In my experience with other libraries, naming that one argument payload instead of value may give the developer a better understanding of what it is: the action payload.

action((context, payload) => { ... })

It's more of a name for the docs because the name itself is not used in the user's code (like context) but may be a good moment to think about it now that it's going to be changed anyway. I'm all in with the overmind approach of making everything as simple to understand as possible.

What do you think?

Usage with HMR

Seems like we'd need a way to patch the current instance. I see some use of hmr in this repo, but no examples. Perhaps a helper function that would go in and replace things properly within the current overmind instance?

wait/debounce do not behave as expected

when running a pipe like:

pipe(
debounce (1000),
mutation(funtion1),
wait(1000),
mutation(function2)
)
expected:
...1 second later
mutation 1
...1 second later
mutation 2

current behavior:
...1 second later
...1 second later (for total of 2 seconds)
mutation 1
mutation 2

try/catch in a pipe

I can't seem to find a way to catch exceptions in a pipe.
For example, if I make an HTTP request (in action or map) and it fails, there is no way to catch it and provide an alternative flow.

I think in Cerebral you could pass a map with success/error keys for the two flows. Maybe something similar could be added here as well?

Add Limitation No IE11 To Overmindjs Home

Very appealing state management toolset!

I spent some time reviewing the Overmindjs site, but it was not until I read the linked to intro article did I see this library depends on js proxy -- not available in IE.

It might be nice for this browser limitation to be indicated on the home page as a heads up to web developers who require IE11 support for their state management tools. Perhaps add a note, or maybe return info from a 'Search ...' for IE11 support on the navbar.

Thanks!

React: Uncaught TypeError: 'get' on proxy: property '_store' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '#<Object>' but got '[object Object]

So this was tricky to debug, because we use Proxies in a few places, so had no idea where this was coming from.

image

Uncaught TypeError: 'get' on proxy: property '_store' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '#<Object>' but got '[object Object]')
    at validateChildKeys (react.development.js:1620)
    at createElementWithValidation (react.development.js:1748)
    at Object.React$$1.createElement (react-hot-loader.development.js:2466)
    at eval (ListItemSimple.tsx:123)
    at renderWithHooks (react-dom.development.js:12840)
    at mountIndeterminateComponent (react-dom.development.js:14817)
    at beginWork (react-dom.development.js:15422)
    at performUnitOfWork (react-dom.development.js:19109)
    at workLoop (react-dom.development.js:19149)
    at HTMLUnknownElement.callCallback (react-dom.development.js:150)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:200)
    at invokeGuardedCallback (react-dom.development.js:257)
    at replayUnitOfWork (react-dom.development.js:18375)
    at renderRoot (react-dom.development.js:19265)
    at performWorkOnRoot (react-dom.development.js:20139)
    at performWork (react-dom.development.js:20051)
    at performSyncWork (react-dom.development.js:20025)
    at requestWork (react-dom.development.js:19894)
    at scheduleWork (react-dom.development.js:19708)
    at dispatchAction (react-dom.development.js:13511)
    at eval (TreeList.tsx:148)

Basically, we are putting some React Elements in Overmind. Perhaps it should be a special case, or warn for them. But as of now it causes this tricky error.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.