Podcast Episode 44
Podcast Episode 44 is up. We have a new guest, Geetika Varma, who recently graduated from BCIT's CST program. We chat about her experiences with studying CS, doing internships and preparing for the Bachelor's degree.
Podcast Episode 44 is up. We have a new guest, Geetika Varma, who recently graduated from BCIT's CST program. We chat about her experiences with studying CS, doing internships and preparing for the Bachelor's degree.
In this episode we are going to refactor our code a little bit, add the ability to both increment and decrement the counter, ability to do so by arbitrary amounts, let the counter start with a predefined value, and we will also add some tests. You can follow along in the blog post, or check out the episode3
tag in the repository, which I'll link in the description. Let's get started.
Ok, to start we will clean up our code a little bit. The old hooks-based code has got to go, and we'll remove all of the bundler logos. We love you vite
, but this is about another star player, namely – Effector.
The code for App.tsx
should look something like this
function App() {
return (
<>
<Counter counter={counterOne} />
<Counter counter={counterTwo} />
</>
);
}
We will also clean up unused imports, and so on…
Now I want to do some clean-ups in the model file. I like to have my factory names capitalized, and my instances to start with a lower case, makes it easier to tell what's the blueprint and what's the actual item that the factory produces.
export const CounterFactory = ...
Now, it would be better if the output type was defined and exported here, so that everyone can just grab it trivially. Let's add it to our model file
export type CounterModel = ReturnType<typeof CounterFactory>;
Let's go back to the App.tsx
and fix the import errors and naming errors.
import { CounterFactory, type CounterModel } from './model';
Now let's fix the invoke
-actions.
const counterOne = invoke(CounterFactory);
const counterTwo = invoke(CounterFactory);
Let us make the component declaration a bit nicer
export const Counter: FunctionComponent<{ counter: CounterModel }> = ({ counter }) => ...
The rest remains the same.
Great, we got rid of the unnecessary things, reduced the noise and now we can focus on adding functionality. For starters, let's make the counter start from an arbitrary value, since sometimes we might want not start with 0
. Let's make the following change in our model file.
export type CounterSettings = {
initialValue?: number;
}
export const CounterFactory = (( initialValue = }: CounterSettings) => {
const $counter = createStore(initialValue);
...
}
Now we can try to set up our second counter with something other than 0
.
In our App.tsx
let's modify the invocations of the factories.
const counterOne = invoke(CounterFactory, {});
const counterTwo = invoke(CounterFactory, { initialValue: 10 });
If we were to refresh the page, the second counter should start at 10
instead of 0
.
Now let's look at having the ability to decrement the counter. It is going to be similar to incrementing it. In our model.ts
let's add the following:
const decrement = createEvent();
sample({
source: $counter,
clock: decrement,
fn(current) {
return current - 1;
},
target: $counter
});
and let's not forget to add that to the factory's output
return {
$counter,
increment,
decrement
}
Now we can go back to our App.tsx
file and wire up the button. Let's get the decrement event from the factory
const { $counter, increment, decrement } = useUnit(counter);
And let's add the button and re-shuffle the card a bit
...
return (
<div className="card">
<button onClick={() => decrement()}>-</button>
<span>Counter value is {$counter}</span>
<button onClick={() => increment()}>+</button>
</div>
);
Now we can verify that our increment and decrement work.
With this in mind, how about we add some functionality to be able to increment and decrement by an arbitrary amount.
There are a few different ways we can do this, but we'll follow the pattern we already have, and maybe get fancier in the later episodes of this series.
For now we need to modify the event payload type. By default any event created is having a parameter type of void
, which translates to “no parameters necessary”. If we wish to pass them, we should be a bit more explicit. Let's modify the increment
and decrement
events in our model file.
const increment = createEvent<number>();
const decrement = createEvent<number>();
Now both events require a payload, but where would we be able to use this payload?
Well for that to work we can pull the payload in the second argument in our sample
's fn
function.
The first argument to fn
will always point to the source
(if present), and the second one will contain the payload for the clock
(if present). There are many different ways one can slice and dice sample, and there's a nice little matrix in the documentation. So let's make those changes now.
sample({
source: $counter,
clock: increment,
fn(current, increment) {
return current + increment;
},
target: $counter
})
sample({
source: $counter,
clock: decrement,
fn(current, decrement) {
return current - decrement;
},
target: $counter
})
And now, because we changed the arity of both our events, we'd have to fix up our template. So let's go back to App.tsx
and make changes there
<button onClick={() => decrement(1)}>-</button>
...
<button onClick={() => increment(1)}>+</button>
We might as well add more buttons with different values
<button onClick={() => decrement(5)}>-5</button>
<button onClick={() => decrement(1)}>-</button>
...
<button onClick={() => increment(1)}>+</button>
<button onClick={() => increment(5)}>+5</button>
Now we can increment or decrement our counters 5 times faster
Now let's add tests. This will show a small sliver of the the goodness that is testing effector business logic. First we need to do some prep
npm install -D vitest
and now let's also add the test command to our package.json
{
"scripts": {
"test": "vitest"
}
}
Let's create our test file
touch src/model.test.ts
And let's start writing some tests
import { beforeEach, describe, expect, test } from "vitest";
import { CounterFactory, type CounterModel } from "./model";
import { allSettled, fork } from "effector";
import { invoke } from "@withease/factories";
Let's make sure that our world starts in correct state by writing some initialization tests
describe("Initialization tests", () => {
test("Counter without initial value is initialized to 0", () => {
const subject = invoke(CounterFactory, {});
const scope = fork();
expect(scope.getState(subject.$counter)).toBe(0);
});
test("Counter with initial value is initialized to that value", () => {
const subject = invoke(CounterFactory, { initialValue: 10 });
const scope = fork();
expect(scope.getState(subject.$counter)).toBe(10);
});
});
Let's run our test suite so we can monitor our progress
npm run test
Okay, everything seems to be going well, let's test the functionality of the increment and decrement
describe("Incrementing behaviours work", () => {
let subject: CounterModel;
beforeEach(() => {
subject = invoke(CounterFactory, {});
});
test("Incrementing by 1 works", async () => {
const scope = fork();
await allSettled(subject.increment, { scope, params: 1 });
expect(scope.getState(subject.$counter)).toBe(1);
});
test("Incrementing by 5 works", async () => {
const scope = fork();
await allSettled(subject.increment, { scope, params: 5 });
expect(scope.getState(subject.$counter)).toBe(5);
});
});
So, there are few things to pay attention to:
fork
and allSettled
. fork
allows us to literally fork the world. And while it's doing so, it can also tweak the world to your liking. Imagine being able to say fork({ values: { $bankBalance: 1_000_000_000 }, handlers: { buyCarFx: () => return 'Maybach' }})
? Wouldn't that be nice? Probably not, but hey, a man can dream. We aren't using that functionality at the moment, we just get the exact copy.allSettled
allows us to push that first domino that starts the computation graph. It waits until all computations (and asynchronous effects, for that matter) resolve. How cool is that? Once they all have resolved – we can poke at the state of the world and make sure it matches our expectations.increment
event. We trigger it with various values and observe that the world has settled in a new state and our expectations were correctLet's add the complementary part for decrement
to ensure that we test everything
describe("Decrementing behaviours work", () => {
let subject: CounterModel;
beforeEach(() => {
subject = invoke(CounterFactory, {});
});
test("Decrementing by 1 works", async () => {
const scope = fork();
await allSettled(subject.decrement, { scope, params: 1 });
expect(scope.getState(subject.$counter)).toBe(-1);
});
test("Decrementing by 5 works", async () => {
const scope = fork();
await allSettled(subject.decrement, { scope, params: 5 });
expect(scope.getState(subject.$counter)).toBe(-5);
});
});
As a last little thing we can see how we can use the both counters to have a quick way to see the total count
I'm not going to build a factory at this point as I'd like to wrap this up, but here's what we can do. In our App.tsx
file we can add a new component and a derived store
const $counterTotal = combine(counterOne.$store, counterTwo.$store, (a, b) => a + b);
const Total: FunctionComponent = () => {
const total = useUnit($counterTotal);
return (<div>Total: ${total}</div>);
}
And now we add it to our main component
function App() {
return (
<>
<Counter counter={counterOne} />
<Counter counter={counterTwo} />
<Total />
</>
);
}
Once we get this we can see our total sum of both counters. And if we have our re-render overlay from react dev tools, we can see that only the parts that were affected by the computation are getting re-rendered. All that without having to do useMemo
In the next episodes of this series we'll start exploring more aspects of effector
. Make sure you check out the blog post which should contain everything from this video pretty much verbatim, get the code from the github repository, and leave a comment if you have any questions that you'd like answered.
I will see you in the next one. I hope you have a great rest of your day. Good bye.
Hello everyone, and welcome to the After Hours. This is the Effector Series, episode 2. In this episode we will be covering the basics of getting Effector added into a react project. All scripts and steps will be published on my blog, which will be linked in the description. We will be using Vite, but usually the bundler choice is not relevant as much to Effector ecosystem. More advanced subjects, such as integration with SSR might require some special consideration and they will be covered separately in future episodes. For the rest of this episode I'll assume that you're on a latest LTS version of node and we'll be using NPM. This should work with other package managers as well, but I'm not going to be covering the differences here.
Link to the Effector documentation: https://effector.dev/
Let's begin by getting the scaffolding going.
npm create vite@latest effector-app -- --template react-ts
cd effector-app
npm install
npm run dev
Excellent, we have an application with a counter implemented using hooks. So let's refactor it with using effector instead.
npm install effector{,-react}
npm install eslint-plugin-effector
Let's update the eslint
configuration. We will be using the recommended
and react
presets.
{
"plugins": ["effector"],
"extends": ["plugin:effector/recommended", "plugin:effector/react"]
}
We also need to add the required imports in our code.
import { createStore, createEvent } from "effector";
import { useUnit } from "effector-react";
Let's create our store that would hold the value for the counter.
const $counter = createStore(0);
Ok, so first thing you would notice, is that the store is created outside of the component. Second, is that there's a $
prefix, which is a suggested naming convention. The eslint
plugin would tell us if we are not naming our units according to it. Stores are prefixed with $
, Effects are postfixed with Fx
and events are simply verbs without any additional sigils. Let's see if we can observe the value of the store by watching it.
$counter.watch(current => console.log("Current value of $counter is", current));
This subscription will execute once the store settles. If we open the console we shall see the output.
Okay, now let's start doing the plumbing. First, we need to get the value of the store displayed on the page. For this purpose we shall be using the useUnit
hook from the effector-react
package. Because this is a hook, it has to reside within the component's scope.
const effectorCount = useUnit($count);
useUnit
is more flexible and allows to wire up multiple stores and events in one call, but we will look at the advanced use cases later.
If we look at the type of effectorCount
, we shall see that is is a number
, which makes sense, since the default value of the store – 0
is a number
.
We can now insert it into our template.
<div className="card">
<button>effectorCount is {effectorCount}</button>
</div>
We now see the button, and it does display “Effector count is 0” indeed. Now let's make it functional. Stores are able to react to events
. So, let's create an event.
const increment = createEvent();
We can actually watch an event as well. In effector ecosystem you can watch any unit
, and stores, events, and effects are all units.
increment.watch(() => console.log('Increment event was triggered'));
Now when we press the button we can see the message in the console.
Let's tie the store and event together with some business logic. There are a couple of ways we can do this, one is more universal, but a bit more complex, another is simple, but does not work for all scenarios.
The simple version looks like this
$count.on(increment, (currentValue) => currentValue + 1);
The more complex version is going to be leveraging one of the most commonly used functions in effector
, namely sample
.
import { sample } from "effector";
Let's look at how wiring it up with sample
is going to look like.
sample({
clock: increment,
source: $count,
fn(current) {
return current + 1;
},
target: $count
});
It reads as follows – “When increment
is triggered, take the current value from $count
, pass it through fn
, and send the result of that call to the $count
store”.
If you were to keep both variants of the code and press the button, the store would increment by 2 (however through the magic of effector, this causes only one re-render).
Now let's take this one step further, and extract all the logic code into it's own file to fully decouple it from the UI.
touch src/model.ts
Here's the contents of our model file
import { createStore, createEvent, sample } from "effector";
export const $count = createStore(0);
export const increment = createEvent();
sample({
clock: increment,
source: $count,
fn: current => current + 1,
target: $count
});
And we'll modify our component code to simply import all the necessary bits from the model.
import { $count, increment } from './model';
const { $count: effectorCount, increment: inc } = useUnit({ $count, increment });
But what if we want to re-use this logic for a separate counter? Well, effector does require to create all links between units ahead of time. It does not work well with dynamically created stores, though there are several developments on that front.
Right now the best way to encapsulate this is by creating a factory. A factory is simply a function that takes parameters and returns some units… In order to make it a bit easier for ourselves we can leverage the @with-ease/factores
library, that helps a bit with this. This library mostly helps in more advanced scenarios, that I will not be covering now, but we might get into the habit right away.
npm install @withease/factories
Let's rewrite our model code to leverage the factory pattern:
import { createStore, createEvent, sample } from "effector";
import { createFactory } from "@withease/factories";
export const counterFactory = createFactory(() => {
const $count = createStore(0);
const increment = createEvent();
sample({
clock: increment,
source: $count,
fn(current) {
return current + 1;
},
target: $count,
});
return { $count, increment };
});
And now let's modify the component
import { invoke } from "@withease/factories";
import { counterFactory } from "./model";
const counterOne = invoke(counterFactory);
And in the component body we'll make the following changes:
const { $count: effectorCount, increment: inc } = useUnit(counterOne);
Now, if we wanted to add another counter, we can do it trivially:
const counterTwo = invoke(counterFactory);
Now we can lift the counter itself into a separate component, that would take the counter business logic via props.
function Counter({ counter }: { counter: ReturnType<typeof counterFactory> }) {
const { $count, increment } = useUnit(counter);
return (
<div className="card">
<button onClick={() => increment()}>Current count is {$count}</button>
</div>
)
}
And now we use it
<Counter counter={counterOne} />
<Counter counter={counterTwo} />
Two fully independent counters backed by the same business logic.
Click here to learn Mo.
In today's episode we go over Mo' Claudius journey from Bachelors in Nigeria and Masters in Canada, to the present day and future PhD aspirations in Computer Science. Check it out!
On giving critical feedback...
Today I'd like to welcome a special guest, Gary Tse. He tells his story about his path in the software engineering, it's ups and downs, doubts and tribulations, and the subsequent rise to the new heights. I hope you enjoy our chat. Check it out!
I'm this episode we talk about the push or pivot dilemma as it applies to asking for help when stuck. We cover the X-Y Problem in some detail. Also, there's a conversation on how helpful seniors can create a state of learned helplessness in their team.
The second half of the episode is dedicated to motivation, burnout, and various thoughts on working on personal projects. Enjoy.
My good friend Ali joins me with the tales of his career path, life at the boot camp, impostor syndrome and burn out. Check it out.
In this one we try to answer another question from my colleague, about preparing for your first job, expectations from juniors, impostor syndrome, etc... Check it out!