Experimental reactive data components for Preact.
This a result of me playing around with Kotlin/Jetpack Compose for a bit, and wondering why our solutions for data and reactivity in (p)react couldn't be at least as elegant or terse as Kotlin's LiveData.
The general premise of this library is that accessing the properties of a Reactive Object automatically subscribes the current component to mutations of those properties:
let user = reactive({ name: 'Bob' });
const Greeting = () => <h1>Hello, {user.name}!</h1>;
render(<Greeting />, document.body);
user.name = 'Alice';
// ^ Greeting is automatically re-rendered with `user.name === 'Alice'`
reactive(obj: object)
: Returns a reactive version of the given object. Accessing its properties automatically subscribes the current component to updates for those properties.
useReactive(obj: object, deps: any[])
: A memoized hook version of reactive()
, useful for non-factory components.
defineProps(props?: string[])
: Returns a reactive object containing the given props (or all props). The returned object will update without re-rendering if its containing component is passed new props.
component((props) => (props) => JSX)
: Creates a factory component (see below)
Factory components are components that consist of an outer "init" function, which returns an inner "render" function:
const Foo = component((props, update) => {
// this is the setup function, which runs only on the first render.
let name = props.name || 'Bob';
let setName = e => update(name = e.target.value);
return props => (
// this is the render function, which runs on _every_ render.
<div>
<h1>Hello {name}</h1>
<label>Name: <input value={name} onInput={setName} /></label>
</div>
);
});
import { component, defineProps, reactive, computed } from 'preact-livedata';
import { render } from 'preact';
const initialTodos = [
{ text: 'one', done: false },
{ text: 'two', done: false },
{ text: 'three', done: false }
];
const App = component(() => {
const state = reactive({ visibility: 'all' });
return () => (
<div>
{["all", "active", "completed"].map(vis =>
<label>
<input type="radio" value={vis} checked={state.visibility === vis} onClick={() => state.visibility = vis} />
{' ' + vis}
</label>
)}
<TodoList {...state} />
</div>
);
});
const TodoList = component(() => {
const props = defineProps(['visibility']);
const todos = reactive(initialTodos);
const filteredTodos = computed(() => todos.filter(todo =>
props.visibility === 'all' || (props.visibility === 'active' ? !todo.done : todo.done)
));
const toggle = todo => todo.done = !todo.done;
const addInput = useRef();
const add = e => {
e.preventDefault();
let input = addInput.current;
todos.push({ text: input.value, done: false });
input.value = '';
};
return () => (
<>
<ul>
{filteredTodos.value.map(todo =>
<Todo key={todo.text} todo={todo} onChange={toggle} />
)}
</ul>
<form onSubmit={add}>
<input placeholder="Add ToDo... [enter]" ref={addInput} />
<button type="submit">Add</button>
</form>
</>
);
});
const Todo = component(() => {
const { todo, onChange } = defineProps();
const toggle = () => onChange(todo);
return () => (
<li>
<label>
<input type="checkbox" checked={todo.done} onClick={toggle} />
{' ' + todo.text}
</label>
</li>
);
});
render(<App />, document.body);