Skip to content

Annotate with comments #7

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 84 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function FancyUserBox(user) {
borderStyle: '1px solid blue',
childContent: [
'Name: ',
// Embed the render output of `NameBox`.
NameBox(user.firstName + ' ' + user.lastName)
]
};
Expand All @@ -56,18 +57,30 @@ To achieve truly reusable features, it is not enough to simply reuse leaves and

```js
function FancyBox(children) {
// `FancyBox` doesn't need to know what's inside it.
// Instead, it accepts `children` as an argument.
return {
borderStyle: '1px solid blue',
children: children
};
}

function UserBox(user) {
// Now we can put different `children` inside `FancyBox` in different parts of UI.
// For example, `UserBox` is a `FancyBox` with a `NameBox` inside.
return FancyBox([
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]);
}

function MessageBox(message) {
// However a `MessageBox` is a `FancyBox` with a message.
return FancyBox([
'You received a new message: ',
message
]);
}
```

## State
Expand Down Expand Up @@ -122,6 +135,8 @@ function memoize(fn) {
};
}

// Has the same API as NameBox but caches its result if its single argument
// has not changed since the last time `MemoizedNameBox` was called.
var MemoizedNameBox = memoize(NameBox);

function NameAndAgeBox(user, currentTime) {
Expand All @@ -132,6 +147,12 @@ function NameAndAgeBox(user, currentTime) {
currentTime - user.dateOfBirth
]);
}

// We calculate the output of `NameAndAgeBox` twice, so it will call `MemoizedNameBox` twice.
// However `NameBox` is only going to be called once because its argument has not changed.
const sebastian = { firstName: 'Sebastian', lastName: 'Markbåge' };
NameAndAgeBox(sebastian, Date.now());
NameAndAgeBox(sebastian, Date.now());
```

## Lists
Expand Down Expand Up @@ -170,13 +191,28 @@ This isn't reducing boilerplate but is at least moving it out of the critical bu

```js
function FancyUserList(users) {
return FancyBox(
UserList.bind(null, users)
);
// `UserList` needs three arguments: `users`, `likesPerUser`, and `updateUserLikes`.

// We want `FancyUserList` to be ignorant of the fact that `UserList` also
// needs `likesPerUser` and `updateUserLikes` so that we don't have to wire
// the arguments for bookkeeping this state through `FancyUserList`.

// We can cheat by only providing the first argument for now:
const children = UserList.bind(null, users)

// Unlike in the previous examples, `children` is a partially applied function
// that still needs `likesPerUser` and `updateUserLikes` to return the real children.

// However, `FancyBox` doesn't "read into" its children and just uses them in its output,
// so we can let some kind of external system inject the missing arguments later.
return FancyBox(children);
}

// The render output is not fully known yet because the state is not injected.
const box = FancyUserList(data.users);
// `box.children()` is a function, so we finally inject the state arguments.
const resolvedChildren = box.children(likesPerUser, updateUserLikes);
// Now we have the final render output.
const resolvedBox = {
...box,
children: resolvedChildren
Expand All @@ -188,34 +224,56 @@ const resolvedBox = {
We know from earlier that once we see repeated patterns we can use composition to avoid reimplementing the same pattern over and over again. We can move the logic of extracting and passing state to a low-level function that we reuse a lot.

```js
// `FancyBoxWithState` receives `children` that are not resolved yet.
// Each child contains a `continuation`. It is a partially applied function
// that would return the child's output, given the child's state and a function to update it.
// The children also contain unique `key`s so that their state can be kept in a map.
function FancyBoxWithState(
children,
stateMap,
updateState
) {
return FancyBox(
children.map(child => child.continuation(
stateMap.get(child.key),
updateState
))
);
// Now that we have the `stateMap`, inject it into all the continuations
// provided by the children to get their resolved rendering output.
const resolvedChildren = children.map(child => child.continuation(
stateMap.get(child.key),
updateState
));

// Pass the rendered output to `FancyBox`.
return FancyBox(resolvedChildren);
}

function UserList(users) {
// `UserList` returns a list of children that expect their state
// to get injected at a later point. We don't know their state yet,
// so we return partially applied functions ("continuations").
return users.map(user => {
continuation: FancyNameBox.bind(null, user),
key: user.id
});
}

function FancyUserList(users) {
return FancyBoxWithState.bind(null,
UserList(users)
);
// `FancyUserList` returns a `continuation` that expects the state
// to get injected at a later point. This state will be passed on
// to `FancyBoxWithState` which needs it to resolve its stateful children.
const continuation = FancyBoxWithState.bind(null, UserList(users));
return continuation;
}

// The render output of `FancyUserList` is not ready to be rendered yet.
// It's a continuation that still expects the state to be injected.
const continuation = FancyUserList(data.users);
continuation(likesPerUser, updateUserLikes);

// Now we can inject the state into it.
const output = continuation(likesPerUser, updateUserLikes);

// `FancyUserList` will forward the state to `FancyBoxWithState`, which will pass
// the individual entries in the map to the `continuation`s of its `children`.

// Those `continuations` were generated in `UserList`, so they will pass
// the state into the individual `FancyNameBox`es in the list.
```

## Memoization Map
Expand All @@ -228,6 +286,12 @@ We can use the same trick we used for state and pass a memoization cache through

```js
function memoize(fn) {
// Note how in the previous memoization example, we kept the cached argument and the
// cached result as a local variable inside `memoize`. This is not useful for lists
// because in a list, the function will be called many times with a different argument.

// Now the function returned by `memoize` accepts the `memoizationCache` as an argument
// in the hope that the list containing a component can supply a "local" cache for each item.
return function(arg, memoizationCache) {
if (memoizationCache.arg === arg) {
return memoizationCache.result;
Expand All @@ -243,13 +307,16 @@ function FancyBoxWithState(
children,
stateMap,
updateState,
memoizationCache
memoizationCacheMap
) {
return FancyBox(
children.map(child => child.continuation(
stateMap.get(child.key),
updateState,
memoizationCache.get(child.key)
// When the UI changes, it usually happens just in some parts of the screen.
// This means that most children with the same keys will likely render to the same output.
// We give each child its own memoization map, so that in the common case its output can be memoized.
memoizationCacheMap.get(child.key)
))
);
}
Expand All @@ -269,6 +336,7 @@ Now, this example is a bit "out there". I'll use [Algebraic Effects](http://math
function ThemeBorderColorRequest() { }

function FancyBox(children) {
// This will propagate through the caller stack, like "throw"
const color = raise new ThemeBorderColorRequest();
return {
borderWidth: '1px',
Expand All @@ -281,6 +349,7 @@ function BlueTheme(children) {
return try {
children();
} catch effect ThemeBorderColorRequest -> [, continuation] {
// However, unlike "throw", we can resume the child function and pass some data
continuation('blue');
}
}
Expand Down