Learn the Basics
Teeny Store brings clarity and control to application state and side effects, making your JavaScript/TypeScript code more predictable and easier to maintain.
Traditionally, in plain JavaScript/Typescript, state is scattered and side effects are freely executed, making the application's data flow unpredictable and difficult to trace. Teeny Store centralizes state and effects, creating a clear flow of data that is easier to control and reason about.
Let's build a very simple calculator app and see how we can use Teeny Store to help us.
In this app, the state includes the two operands, the operator, and the calculation result. We can create a Teeny Store to manage that state by using the createStore function.
import { createStore } from "@vuezy/teeny-store";
const store = createStore({
operand1: 0,
operand2: 0,
operator: '+',
result: 0,
});To update the state, we can use the setState method like this.
store.setState((state) => ({ ...state, operator: '-' }));Or, we can also define some action functions that abstract the logic for updating the state in the store. In our calculator app, there are four actions that can modify the state.
const store = createStore({
operand1: 0,
operand2: 0,
operator: '+',
result: 0,
}, {
setOperand1: (state, setState, operand) => {
setState((state) => ({ ...state, operand1: operand }));
},
setOperand2: (state, setState, operand) => {
setState((state) => ({ ...state, operand2: operand }));
},
setOperator: (state, setState, operator) => {
setState((state) => ({ ...state, operator }));
},
calculate: (state, setState) => {
let result = NaN;
if (state.operator === '+') {
result = parseFloat(state.operand1) + parseFloat(state.operand2);
} else if (state.operator === '-') {
result = parseFloat(state.operand1) - parseFloat(state.operand2);
} else if (state.operator === 'x') {
result = parseFloat(state.operand1) * parseFloat(state.operand2);
} else if (state.operator === '/') {
result = parseFloat(state.operand1) / parseFloat(state.operand2);
}
setState((state) => ({ ...state, result }));
},
});NOTE
It is recommended that the state object is treated as immutable. Teeny Store relies on shallow reference comparison to detect changes and execute side effects. So, instead of modifying the original state object, we should create a new object with the updated values.
Let's set up event listeners to call these actions.
function updateOperand1(event) {
store.actions.setOperand1(event.target.value);
store.actions.calculate();
}
function updateOperand2(event) {
store.actions.setOperand2(event.target.value);
store.actions.calculate();
}
function updateOperator(event) {
store.actions.setOperator(event.target.textContent);
store.actions.calculate();
}
document.getElementById('operand1')?.addEventListener('change', updateOperand1);
document.getElementById('operand2')?.addEventListener('change', updateOperand2);
const operatorBtns = document.getElementsByClassName('operator-btn');
for (let i = 0; i < operatorBtns.length; i++) {
operatorBtns[i].addEventListener('click', updateOperator);
}Next, we need to define the application's behavior in response to state changes. For example, when the operator state changes, the displayed operator must be updated and the result state must be recalculated. This is known as an effect, or more precisely, a side effect.
Here is the list of effects that react to state changes in our calculator.
- On
operand1oroperand2change: Update the value in the corresponding input field. - On
operatorchange: Update the displayed operator symbol. - On
operand1,operand2, oroperatorchange: Recalculate the result. - On
resultchange: Display the new result.
Let's implement these effects using the useEffect method of the store and carefully specify the dependency arrays to control when they run.
store.useEffect((state) => renderOperand('operand1', state.operand1), (state) => [state.operand1]);
store.useEffect((state) => renderOperand('operand2', state.operand2), (state) => [state.operand2]);
store.useEffect(renderOperator, (state) => [state.operator]);
store.useEffect(store.actions.calculate, (state) => [state.operand1, state.operand2, state.operator]);
store.useEffect(renderResult, (state) => [state.result]);
function renderOperand(id, value) {
const operandInput = document.getElementById(id);
if (operandInput) {
operandInput.value = value;
}
}
function renderOperator(state) {
const operatorEl = document.getElementById('operator');
if (operatorEl) {
operatorEl.textContent = state.operator;
}
}
function renderResult(state) {
const resultEl = document.getElementById('result');
if (resultEl) {
resultEl.textContent = state.result;
}
}We now no longer need to explicitly call the calculate action whenever the operand or the operator is updated. Having properly controlled effects makes our code more predictable.
NOTE
When we call the setState method, effects will be triggered, but not necessarily executed. Teeny Store performs a shallow comparison on each dependency to determine whether an effect should execute.
Effect execution is batched together with other state updates and by default scheduled to run after the call stack is empty. Also note that effects are executed in the order they are defined.
TIP
The effect's dependency array can technically contain any value. However, most of the time, it's better to limit the dependency array to only include the state the effect depends on.
If you want, we can also use the compute method of the store to track the calculation result. The compute method is pretty similar to the useEffect method. It helps us compute a value (the calculation result) in response to dependency changes. The result will be available in the computed property of the store.
const store = createStore({
operand1: 0,
operand2: 0,
operator: '+',
result: 0,
}, {
// ...
calculate: (state, setState) => {
// ...
},
});
store.compute('result', (state) => {
if (state.operator === '+') {
return parseFloat(state.operand1) + parseFloat(state.operand2);
}
if (state.operator === '-') {
return parseFloat(state.operand1) - parseFloat(state.operand2);
}
if (state.operator === 'x') {
return parseFloat(state.operand1) * parseFloat(state.operand2);
}
if (state.operator === '/') {
return parseFloat(state.operand1) / parseFloat(state.operand2);
}
return NaN;
}, (state) => [state.operand1, state.operand2, state.operator]);
// ...
store.useEffect(store.actions.calculate, (state) => [state.operand1, state.operand2, state.operator]);
store.useEffect(renderResult, (state) => [state.result]);
store.useEffect(renderResult, () => [store.computed.result]);
// ...
function renderResult(state) {
const resultEl = document.getElementById('result');
if (resultEl) {
resultEl.textContent = state.result;
resultEl.textContent = store.computed.result;
}
}One last thing—let's persist the state to the localStorage so it survives a page reload. To do that, we will have to configure the store instance to use the persistence plugin.
import { createPersistencePlugin, defineStore } from "@vuezy/teeny-store";
const store = defineStore({ operand1: 0, operand2: 0, operator: '+' }, {
// ...
}).use(createPersistencePlugin({
storage: 'localStorage',
key: 'calculator',
})).create();The calculator app is now working as expected. Here is the full code.
import { createPersistencePlugin, defineStore } from "@vuezy/teeny-store";
const store = defineStore({ operand1: 0, operand2: 0, operator: '+' }, {
setOperand1: (state, setState, operand) => {
setState((state) => ({ ...state, operand1: operand }));
},
setOperand2: (state, setState, operand) => {
setState((state) => ({ ...state, operand2: operand }));
},
setOperator: (state, setState, operator) => {
setState((state) => ({ ...state, operator }));
},
}).use(createPersistencePlugin({
storage: 'localStorage',
key: 'calculator',
})).create();
store.compute('result', (state) => {
if (state.operator === '+') {
return parseFloat(state.operand1) + parseFloat(state.operand2);
}
if (state.operator === '-') {
return parseFloat(state.operand1) - parseFloat(state.operand2);
}
if (state.operator === 'x') {
return parseFloat(state.operand1) * parseFloat(state.operand2);
}
if (state.operator === '/') {
return parseFloat(state.operand1) / parseFloat(state.operand2);
}
return NaN;
}, (state) => [state.operand1, state.operand2, state.operator]);
store.useEffect((state) => renderOperand('operand1', state.operand1), (state) => [state.operand1]);
store.useEffect((state) => renderOperand('operand2', state.operand2), (state) => [state.operand2]);
store.useEffect(renderOperator, (state) => [state.operator]);
store.useEffect(renderResult, () => [store.computed.result]);
function renderOperand(id, value) {
const operandInput = document.getElementById(id);
if (operandInput) {
operandInput.value = value;
}
}
function renderOperator(state) {
const operatorEl = document.getElementById('operator');
if (operatorEl) {
operatorEl.textContent = state.operator;
}
}
function renderResult() {
const resultEl = document.getElementById('result');
if (resultEl) {
resultEl.textContent = store.computed.result;
}
}
function updateOperand1(event) {
store.actions.setOperand1(event.target.value);
}
function updateOperand2(event) {
store.actions.setOperand2(event.target.value);
}
function updateOperator(event) {
store.actions.setOperator(event.target.textContent);
}
document.getElementById('operand1')?.addEventListener('change', updateOperand1);
document.getElementById('operand2')?.addEventListener('change', updateOperand2);
const operatorBtns = document.getElementsByClassName('operator-btn');
for (let i = 0; i < operatorBtns.length; i++) {
operatorBtns[i].addEventListener('click', updateOperator);
}For details on all available options and methods, see the API Reference.
See also: Examples.
