Skip to content

Latest commit

 

History

History
529 lines (386 loc) · 12 KB

api.md

File metadata and controls

529 lines (386 loc) · 12 KB

API

Imperative framework

on

The next basic abstraction is expression. Expression is a function that read reactive boxes or selectors. It can return value and write reactive values inside.

We can subscribe to change any reactive expression using on function (which also works with signal). play on runkit.

const { get, set } = value(0);

const next = () => get() + 1;

on(next, (val, prev) => console.log(val, prev));

set(5); // We will see 6 and 1 in developer console output, It are new and previous value

In that example expression is next function, because It get value and return that plus one.

The reactive container instance is also available as first argument of on function. play on runkit.

const count = value(0);

const next = count.map(v => v + 1);

on(next, (val, prev) => console.log(val, prev));

count(5); // We will see 6 and 1 in developer console output, It are new and previous value

on.once

Subscribe listener to reactive expression only for one time. After it listener will be unsubscribed. play on runkit

const count = value(0);
on.once(count, (val) => console.log(val));

count(1); // in console: 1
count(2); // nothing in console because once reaction already been

sync

const source = value(0);
const target = value(0);

sync(source, target);
// same as sync(() => source.get(), val => target(val));

source.set(10);

console.log(target.val) // 10

Play on runkit

cycle

const { get, set } = value(0);

cycle(() => {
  console.log(get() + 1);
});

set(1);
set(2);

// In output of developer console will be 1, 2 and 3.

Play on runkit

  • Takes a function as reactive expression.
  • After each run: subscribe to all reactive values accessed while running
  • Re-run on data changes

Class decorators for TRFP

prop

prop - reactive value marker decorator. Each reactive value has an immutable state. If the immutable state will update, all who depend on It will refresh.

class Todos {
  @prop items = [];

  constructor() {
    on(() => this.items, () => console.log('items changed'));
  }

  add(todo: string) {
    this.items = this.items.concat(todo); // an immutable modification
  }
}

cache

cache - is the decorator for define selector on class getter.

class Todos {
  @prop items = [];

  @cache get completed() {
    return this.items.filter(item => item.completed);
  }
}

Shared technique

shared

The function for providing an instance of single instantiated shared dependency with global availability. You can use class or function as a dependency creator.

const loader = () => {
  const count = value(0);
  return {
    active: count.select(state => state > 0),
    start: () => count.val++,
    stop: () => count.val--
  }
}

const sharedLoader = () => shared(loader);

// And every where in your add

const App = ({ children }) => {
  const loaderActiveState = useValue(sharedLoader().active);
  return <>
    {loaderActiveState ? 'loading...' : children}
  </>
}

initial

Define initial value that can be pass to the first argument of shared constructor or function.

const rootStore = (init) => {
  const store = value(init);
  return {
    user: store.select(state => state.user)
  }
}

initial({ user: 'Joe' })
console.log(shared(rootStore).user.val) // in console: Joe

free

Clean all cached shared instances. It's usually needed for testing or server-side rendering. Has no parameters. play on runkit

const Shared = () => {
  console.log('initialized');
  un(() => console.log('destroyed'));
}

shared(Shared); // in console: initialized
free();         // in console: destroyed

mock

Define resolved value for any shareds. Necessary for unit tests.

const mocked = mock(Shared, {
  run: jest.fn()
});

shared(Shared).run();
expect(mocked.run).toHaveBeenCalled();

unmock

Reset mocked value from shared. Possible to pass as many arguments as you need.

mock(A, {});
mock(B, {});

unmock(A, B);

Unsubscribe scopes control

un

Register a custom unsubscriber for shared, local, or scoped instances. play on codesandbox

const formLogic = () => {
  console.log('initialized');
  un(() => console.log('destroyed'));
}

const Form = () => {
  const form = useLocal(formLogic);
  // ...
}

const App = observe(() => {
  const opened = useLocal(() => value(false));
  const toggle = useLocal(() => opened.updater(state => !state), [opened]);

  return <>
    {opened.val ? <Form /> : null}
    <button onClick={toggle}>toggle</button>
  </>
})

isolate

It's a a primary way to manual control of unsubscribers. Usually automatic unsubscribe scopes are shared, scoped, and local. play on runkit

const logic = () => {
  // Collect all unsubscribers inside
  const unsub = isolate(() => {

    // Register unsubscriber console logger
    un(() => console.log('unsub'));
  });
  return unsub
}

const unsub = shared(logic);
free(); // nothing in console, because our unsubscribe listener was isolate.
unsub(); // in console: unsub

Async api

pool

The pool function provides the creation of a special function that detects started and finished asynchronous queries and performs information to "pending" value property. play on runkit

const load = pool(async (id) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  return await response.json();
});

const promise = load(1);
console.log(load.pending.val) // in console: true

The pool using in real world you can see in the simple form example.

Track and transactions

transaction

If you need to run several assignments with only one dependency recalculation at the finish of. You should use the transaction function. play on runkit

const a = value(0);
const b = value(0);

on(() => a.val + b.val, sum => console.log('sum', sum));

transaction(() => {
  a.val = 1;
  b.val = 1;
});  // in console only one reaction with sum equals: 2

transaction.func

You can make a transaction function with it.

const a = value(0);
const b = value(0);

on(() => a.val + b.val, sum => console.log('sum', sum));

const run = transaction.func(() => {
  a.val = 1;
  b.val = 1;
});

run(); // in console only one reaction with sum equals: 2

untrack

If you need reading reactive value without reactive dependency creation, the untrack function is your choice.

const a = value(0);
const b = value(0);

on(() => a.val + untrack(() => b.val), sum => console.log('sum', sum));
a(2) // in console: sum 2
b(1) // nothing in console because the reactive value `b` is untracked
a(3) // in console: sum 4

untrack.func

You can make any function untracked with it.

const a = value(0);
const b = value(0);

const sum = untrack.func(() => a.val + b.val);

on(sum, () => console.log('sum'));
a(1) // nothing in console because the reactive value `a` is untracked
b(1) // nothing in console too for the same reason

React bindings

observe

const name = value('Joe');
const change = name.updater((state) => state === 'Joe' ? 'Mike' : 'Joe');

const App = observe(() => {
  return <>
    <p>name: {name.val}</p>
    <button onClick={change}>change</button>
  </>
})

observe.nomemo

An observed component by default wrapped to React.memo for performance reason. But you can get an observed component without it if you need ref forwarding for example.

const Area = React.forwardRef(
  observe.nomemo((props, ref) => (
    <div ref={ref}>
      {props.children}
    </div>
  ))
);

useValue

const count = value(0);
const inc = value.updater((state) => state + 1);

const App = () => {
  const countState = useValue(count);
  const nextState = useValue(() => count.val + 1);

  return <>
    <p>count: {countState}</p>
    <p>next: {nextState}</p>
    <button onClick={inc}>inc</button>
  </>
}

useValues

const name = value('Joe')
const secret = value('xx')

const change = () => {
  name.val += 'e';
  secret.val += 'x';
}

const App = () => {
  const values = useValues({ name, secret });
  return <>
    <p>name: {values.name}</p>
    <p>secret: {value.secret}</p>
    <button onClick={change}>change</button>
  </>
}

useShared

Alias for shared function. The globally available logic. play on codesandbox

const counterLogic = () => {
  const count = value(0);
  const inc = () => count.val += 1;
  const dec = () => count.val -= 1;
  return { count, inc, dec };
};

const State = observe(() => {
  const { count } = useShared(counterLogic);
  return <p>{count.val}</p>
});
const Buttons = () => {
  const { inc, dec } = useShared(counterLogic);
  return (
    <>
      <button onClick={inc}>+</button>
      <button onClick={dec}>-</button>
    </>
  );
}

const App = () => <>
  <State />
  <Buttons />
  <State />
  <Buttons />
</>

useLocal

React component's local logic availability. play on codesandbox

const counterLogic = () => {
  const count = value(0)
  const inc = () => count.val += 1

  return { count, inc }
}

const Counter = observe(() => {
  const logic = useLocal(counterLogic);

  return <p>
    {logic.count.val} <button onClick={logic.inc}>+</button>
  </p>
})

const App = () => <>
  <Counter />
  <Counter />
</>

useJsx

You can connect your reactivity to React using a new component locally defined in yours. All reactive values read in that place will be subscribed. Each time when receiving new values, a locally defined component will be updated, and only one, without any rerendering to owner component. It can be used as a performance optimization for rerendering as smaller pieces of your component tree as it possible. play on codesandbox

const count = value(0);
// ...
const App = () => {
  const Body = useJsx(() => {
    // Make reactive dependency by reading value's "val" property
    const val = count.val;

    if (val === 0) return <>{val} zero</>;
    if (val > 0) return <b>{val} positive</b>;
    return <i>{val} negative</i>;
  });

  return (
    <p>
      <button onClick={api.inc}>+</button>
      <button onClick={api.dec}>-</button>
      <Body />
    </p>
  );
};