@semantic-ui/reactivity
This is a paired down signal/reactivity library loosely inspired by Tracker from Meteor JS. It is used to control reactive updates from Semantic and is built into its templating system.
Signal
lets you define a variable which will trigger any Reaction
or functions called by a reaction to retrigger when it is modified.
Reaction
is a reactive context, this will rerun when referenced reactive values are modified.
Reactions are enqueued and then flushed using the microtask queue
Basic Usage
You can create a reaction by simply creating a variable then modifying its value.
import { Signal, Reaction } from '@semantic-ui/reactivity';
let reactiveValue = new Signal('first');
Reaction.create(computation => {
console.log(reactiveValue.get());
});
saying.value = 'second'
saying.set('second');
Any computation will receive itself as the first parameter of its callback, which you use to do things like check if its the firstRun
or call stop()
to stop the computation.
import { Signal, Reaction } from '@semantic-ui/reactivity';
let saying = new Signal('hello');
Reaction.create(comp => {
if(comp.firstRun) {
console.log('First run!');
}
if(saying.value == 'goodbye') {
comp.stop();
}
});
saying.set('goodbye');
Equality
When declaring a reactive variable you can pass in an equality function that will be used to determine if calculations should be rerun when the value is updated. The default function is isEqual
from utils which will handle most common cases like dates, binary data and deep object equality.
Objects
let obj1 = { name: 'Sally', age: 22 };
let obj2 = { name: 'Sally', age: 22 };
let reactiveObj = new Signal(obj1);
Reaction.create(comp => {
console.log(reactiveObj.get());
});
reactiveObj.set(obj2);
let obj1 = { name: 'Sally', age: 22 };
let obj2 = { name: 'Sally', age: 23 };
let reactiveObj = new Signal(obj1);
Reaction.create(comp => {
console.log(reactiveObj.get());
});
reactiveObj.set(obj2);
const customIsEqual = (a, b) => {
return false;
}
let reactiveObj = new Signal({ name: 'Sally', age: 22 }, customIsEqual);
Reaction.create(comp => {
const obj = reactiveObj.get();
console.log('Log');
});
reactiveObj.set(reactiveObj.get());
Property / Array Mutations
You can use the set
helper to declaratively update values like arrays and objects, which would not normally trigger reactivity if you simply modify their value
.
Objects
import { Signal, Reaction } from '@semantic-ui/reactivity';
let person = {
name: 'Jack',
age: 32,
}
let reactivePerson = new Signal(person);
Reaction.create(comp => {
console.log(reactivePerson.get().name);
});
person.name = 'Jill';
reactivePerson.set(person);
Arrays
let rows = [
{ name: 'Sally', age: 22 },
{ name: 'Jack', age: 32 }
];
let reactiveRows = new Signal(rows);
Reaction.create(comp => {
console.log(reactiveRows.get().length);
});
rows.pop();
reactiveRows.set(rows);
const numbers = new Signal([10, 20, 30]);
numbers.push(40);
numbers.unshift(0);
numbers.splice(0, 2);
numbers.setIndex(1, 99);
numbers.removeIndex(1);
Booleans
Boolean helpers allow you to toggle the state of a Signal that holds a boolean value.
const isToggled = new Signal(false);
isToggled.toggle();
console.log('Value is now true');
isToggled.toggle();
console.log('Value is now false again');
First Run
You can use firstRun
to determine if this calculation is running from an initial value being set. Keep in mind though if you leave the function early on first run it will never set up a reactive reference to unreachable code.
import { Signal, Reaction } from '@semantic-ui/reactivity';
let saying = new Signal('hello');
Reaction.create(comp => {
if(comp.firstRun) {
return;
}
let saying = saying.get();
console.log(saying);
});
Reaction.create(comp => {
let saying = saying.get();
if(comp.firstRun) {
return;
}
console.log(saying);
});
saying.set('goodbye');
Controlling Reactivity
Guard
You can help fine-tune reactivity by using guard to only pay attention to certain parts of a reactive context
const userAge = new Signal(30);
const userName = new Signal('John Doe');
const lastUpdated = new Signal(new Date());
const getUserInfo = () => {
return {
name: userName,
age: userAge,
date: lastUpdated,
};
};
Reaction.create((comp) => {
Reaction.guard(() => {
let user = getUserInfo();
return {
name: user.name,
age: user.age,
};
});
if(!comp.firstRun) {
console.log(`User Info Updated: Name: ${userInfo.name}, Age: ${userInfo.age}`);
}
setTimeout(() => {
userName.value = 'Jane Doe';
}, 300);
setTimeout(() => {
userAge.value = 31;
}, 1000);
setTimeout(() => {
lastUpdated.value = new Date();
}, 2000);
});
Peeking at Current Value
To get the current value of a Signal
without establishing a reactive dependency, use the peek()
method. This is particularly useful when you need to access the value for read-only purposes outside of a reactive computation and do not want to trigger reactivity.
const counter = new Signal(10);
const currentValue = counter.peek();
console.log(`Current value without establishing dependency: ${currentValue}`);
Nonreactive
The Reaction.nonreactive
function allows you to perform computations or access reactive variables without establishing a reactive dependency. This is useful when you need to read from a reactive source but don't want the surrounding computation to re-run when the source changes.
const reactiveValue = new Signal('Initial Value');
Reaction.nonreactive(() => {
const value = reactiveValue.get();
console.log(`Read inside nonreactive: ${value}`);
});
reactiveValue.set('Updated Value');
Flushing Changes
When a Signal
updates an update is enqueued and flushes asynchronously when the microtask queue is processed. This means that intermediary values will not be processed when updating code in a loop.
You can trigger the queue to be immediately flushed to prevent this by using the Reaction.flush()
helper.
import { Signal, Reaction } from '@semantic-ui/reactivity';
let number = new Signal(1);
Reaction.create(comp => {
console.log(number.get());
});
[1,2,3,4,5].forEach(value => number.set(value));
let number = new Signal(1);
Reaction.create(comp => {
console.log(number.get());
});
[1,2,3,4,5].forEach(value => {
number.set(value);
Reaction.flush();
});
Accessing Computation
You can access the current computation either using the returned value from create
or as the first parameter of the callback.
This can be helpful to inspect the listeners or to stop the computation using the stop
method.
import { Signal, Reaction } from '@semantic-ui/reactivity';
let number = new Signal(1);
Reaction.create(comp => {
if(number.get() > 3) {
comp.stop();
return;
}
console.log(number.get());
});
let comp = Reaction.create(() => {
if(number.get() > 3) {
comp.stop();
return;
}
console.log(number.get());
});
[1,2,3,4,5].forEach(value => {
number.set(value);
Reaction.flush();
});
Helper Functions
Numbers
Signal
includes a couple helpers numbers
let count = new Signal(0);
count.increment();
let count = new Signal(0);
count.increment(2);
let count = new Signal(2);
count.decrement();
let count = new Signal(0);
count.decrement(2);
Date
Signal
includes a helper to make dates asier
let date = new Signal(new Date());
setTimeout(() => {
date.now();
}, 1000);
Array Mutation Helpers
Signal
includes a few helpers for some of the most common usecases for manipulating arrays
let items = new Signal([0,1,2]);
items.removeIndex(1);
let items = new Signal([0,2,2]);
items.setIndex(1);
let items = new Signal([0,1,2]);
items.unshift();
let items = new Signal([0,1,2]);
items.push(3);
Array of Objects
Signal
provides several helpers for manipulating arrays of objects a common data structure when handling structured data.
const tasks = new Signal([
{ _id: 'task1_uuid', task: 'Implement feature', completed: true }
{ _id: 'task2_uuid', task: 'Write Tests', completed: true }
{ _id: 'task3_uuid', task: 'Write documentation', completed: false },
]);
tasks.setProperty('task3_uuid', 'completed', true);
const newTask = { _id: 'tasks1_uuid', task: 'Reimplement feature', completed: false };
tasks.replaceItem('task1_uuid', newTask)
const index = tasks.getIndex('tasks1_uuid')
const id = tasks.getID(tasks.get()[0])
tasks.removeItem('tasks2_uuid')