What does Cortex.Net react to?
Cortex.Net usually reacts to exactly the things you expect it to. Which means that in 90% of your use cases Cortex.Net "just works". However, at some point you will encounter a case where it might not do what you expected. At that point it is invaluable to understand how Cortex.Net determines what to react to.
Cortex.Net reacts to any existing observable property that is read during the execution of a tracked function.
- "reading" is dereferencing an object's property, which can be done through "dotting into" it (eg.
User.Name
). - "tracked functions" are the expression of
Computed
, theBuildRenderTree
method of an Observer component, and the functions that are passed as the first param toWhen
,Reaction
andAutorun
. - "during" means that only those observables that are being read while the function is executing are tracked. It doesn't matter whether these values are used directly or indirectly by the tracked function.
In other words, Cortex.Net will not react to:
- Values that are obtained from observables, but outside a tracked function
- Observables that are read in an asynchronously invoked code block
Cortex.Net tracks property access, not values
To elaborate on the above rules with an example, suppose that you have the following observable data structure
(Observable
applies to all properties when applied on the class, so all properties in this example are observable):
using Cortex.Net;
using Cortex.Net.Api;
var sharedState = SharedState.GlobalState;
[Observable]
class Message
{
string Title { get; set; }
Author Author { get; set; }
ICollection<string> Likes { get; set; }
}
[Observable]
class Author
{
string Name { get; set; }
}
var message = new Message() {
Title = "Foo",
Author = new Author() {
Name = "Michel",
},
Likes = sharedState.Collection(new [] {"John", "Sara" })
});
In memory that looks as follows. The green boxes indicate observable properties. Note that the values themselves are not observable!
Now what Cortex.Net basically does is recording which arrows you use in your function. After that, it will re-run whenever one of these arrows changes; when they start to refer to something else.
Examples
Let's show that with a bunch of examples (based on the message
variable defined above):
Correct: dereference inside the tracked function
sharedState.Autorun(r => {
Console.WriteLine(message.Title);
})
message.Title = "Bar";
This will react as expected, the .Title
property was dereferenced by the Autorun
, and changed afterwards, so this
change is detected.
You can verify what Cortex.Net will track by calling Trace()
inside the tracked
function. In the case of the above function it will output the following:
var disposable = sharedState.Autorun(r => {
Console.WriteLine(message.Title);
r.Trace();
})
// Outputs:
// [Cortex.Net] 'Autorun@2' tracing enabled
message.Title = "Hello";
// [Cortex.Net] 'Autorun@2' is invalidated due to a change in: 'Message@1.Title'
Incorrect: changing a non-observable reference
sharedState.Autorun(r => {
Console.WriteLine(message.Title);
})
message = new Message()
{
Title = "Bar",
};
This will not react. message
was changed, but message
is not an observable, just a variable which refers to
an observable, but the variable (reference) itself is not observable.
Incorrect: dereference outside a tracked function
var title = message.Title;
sharedState.Autorun(r => {
Console.WriteLine(title);
});
message.Title = "Bar";
This will not react. message.Title
was dereferenced outside the Autorun
, and just contains the value of
message.Title
at the moment of dereferencing (the string "Foo"
).
Title
is not an observable so Autorun
will never react.
Correct: dereference inside the tracked function
sharedState.Autorun(r => {
Console.WriteLine(Message.Author.Name);
});
Message.Author.Name = "Sara";
Message.Author = new Author() { Name: "John" };
This will react to both changes. Both Author
and Author.Name
are dotted into, allowing Cortex.Net to track
these references.
Incorrect: store a local reference to an observable object without tracking
var author = message.Author;
sharedState.Autorun(r => {
Console.WriteLine(author.Name);
});
Message.Author.Name = "Sara";
Message.Author = new Author() { Name: "John" };
The first change will be picked up, message.Author
and author
are the same object, and the .Name
property is
dereferenced in the autorun. However the second change will not be picked up, the message.Author
relation is not
tracked by the Autorun
. Autorun is still using the "old" author
.
Correct: access observable collection properties in tracked function
sharedState.Autorun(r => {
Console.WriteLine(messages.Likes.Count);
});
message.Likes.Add("Jennifer");
This will react as expected. .Count
counts towards a property.
Note that this will react to any change in the collection.
Arrays are not tracked per index / property (like observable objects and dictionaries) but as a whole.
Incorrect: access out-of-bounds indices in tracked function
sharedState.Autorun(r => {
Console.WriteLine(messages.Likes[0]);
});
message.Likes.Add("Jennifer");
This will react with the above sample data, array indexers count as property access. But only if the provided
index < Count
. Cortex.Net will not track not-yet-existing indices or object properties
(except when using Dictionaries). So always guard your array index based access with a .Count
check.
Correct: access Collection enumerator in tracked function
sharedState.Autorun(r => {
Console.WriteLine(string.Join(", ", message.Likes));
});
message.Likes.Add("Jennifer");
This will react as expected. All methods that access the Collection through an enumerator are tracked automatically.
sharedState.Autorun(r => {
Console.WriteLine(string.Join(", ", message.Likes));
});
message.Likes[2] = "Jennifer";
This will react as expected. All index assignments are detected, but only if index <= Count
.
Correct: using not yet existing dictionary entries
var twitterUrls = sharedState.Dictionary(new Dictionary() {{
"John", "twitter.com/johnny"
}});
sharedState.Autorun(r => {
if (twitterUrls.ContainsKey("Sara"))
{
Console.WriteLine(twitterUrls["Sara"]);
}
});
twitterUrls.Add("Sara", "twitter.com/horsejs");
This will react. Observable dictionaries support observing entries that may not exist.
You can check for the existence of an entry first by using twitterUrls.ContainsKey("Sara")
.
So for dynamically keyed collections, always use observable dictionaries.
The section below has not been adapted to C# yet.
Cortex.Net only tracks synchronously accessed data
function upperCaseAuthorName(author) {
const baseName = author.name
return baseName.toUpperCase()
}
autorun(() => {
console.log(upperCaseAuthorName(message.author))
})
message.author.name = "Chesterton"
This will react. Even though author.name
is not dereferenced by the thunk passed to autorun
itself,
Cortex.Net will still track the dereferencing that happens in upperCaseAuthorName
,
because it happens during the execution of the autorun.
autorun(() => {
setTimeout(() => console.log(message.likes.join(", ")), 10)
})
message.likes.push("Jennifer")
This will not react, during the execution of the autorun
no observables where accessed, only during the setTimeout
.
In general this is quite obvious and rarely causes issues.
Cortex.Net only tracks data accessed for observer
components if they are directly accessed by render
A common mistake made with observer
is that it doesn't track data that syntactically seems parent of the observer
component, but in practice is actually rendered out by a different component. This often happens when render callbacks
of components are passed in first class to another component.
Take for example the following contrived example:
const MyComponent = observer(({ message }) => (
<SomeContainer title={() => <div>{message.title}</div>} />
))
message.title = "Bar"
At first glance everything might seem ok here, except that the <div>
is actually not rendered by MyComponent
(which has a tracked rendering), but by SomeContainer
.
So to make sure that the title of SomeContainer
correctly reacts to a new message.title
, SomeContainer
should be an observer
as well.
If SomeContainer
comes from an external lib, this is often not under your own control. In that case you can address
this by either wrapping the div
in its own stateless observer
based component, or by leveraging the <Observer>
component:
const MyComponent = observer(({ message }) =>
<SomeContainer
title = {() => <TitleRenderer message={message} />}
/>
)
const TitleRenderer = observer(({ message }) =>
<div>{message.title}</div>}
)
message.title = "Bar"
Alternatively, to avoid creating additional components, it is also possible to use the Cortex.Net-react built-in
Observer
component, which takes no arguments, and a single render function as children:
const MyComponent = ({ message }) => (
<SomeContainer title={() => <Observer>{() => <div>{message.title}</div>}</Observer>} />
)
message.title = "Bar"
Avoid caching observables in local fields
A common mistake is to store local variables that dereference observables, and then expect components to react. For example:
@observer
class MyComponent extends React.component {
author
constructor(props) {
super(props)
this.author = props.message.author
}
render() {
return <div>{this.author.name}</div>
}
}
This component will react to changes in the author
's name, but it won't react to changing the .author
of the message
itself! Because that dereferencing happened outside render()
,
which is the only tracked function of an observer
component.
Note that even marking the author
component field as @observable
field does not solve this; that field is still assigned only once.
This can simply be solved by doing the dereferencing inside render()
, or by introducing a computed property on the component instance:
@observer class MyComponent extends React.component {
@computed get author() {
return this.props.message.author
}
// ...
How multiple components will render
Suppose that the following components are used to render our above message
object.
const Message = observer(({ message }) => (
<div>
{message.title}
<Author author={message.author} />
<Likes likes={message.likes} />
</div>
))
const Author = observer(({ author }) => <span>{author.name}</span>)
const Likes = observer(({ likes }) => (
<ul>
{likes.map(like => (
<li>{like}</li>
))}
</ul>
))
change | re-rendering component |
---|---|
message.title = "Bar" |
Message |
message.author.name = "Susan" |
Author (.author is dereferenced in Message , but didn't change)* |
message.author = { name: "Susan"} |
Message , Author |
message.likes[0] = "Michel" |
Likes |
Notes:
- * If the
Author
component was invoked like:<Author author={ message.author.name} />
. ThenMessage
would be the dereferencing component and react to changes tomessage.author.name
. Nonetheless<Author>
would rerender as well, because it receives a new value. So performance wise it is best to dereference as late as possible. - ** If likes were objects instead of strings, and if they were rendered by their own
Like
component, theLikes
component would not rerender for changes happening inside a specific like.
TL;DR
Cortex.Net reacts to any existing observable property that is read during the execution of a tracked function.