Skip to content

Updating Objects in State

States( ReactterState ) like Signal , UseState , UseAsyncState , UseReducer and UseCompute  can hold any type of Dart value, including objects. However, you should not modify objects held in the Reactter state directly, when you need to update an object, create a new one or make a copy of the existing object, and then set the state to use this new or copied object.

What is a mutation?

You can store any kind of Dart value in state.

final count = Signal(0);

So far you’ve been working with numbers, strings, and booleans. These kinds of Dart values are immutable, meaning unchangeable or read-only. You can trigger a re-render to replace a value:

count.value = 2;

The count state changed from 0 to 2 , but the number 0 itself did not change. It’s not possible to make any changes to the built-in primitive values like numbers, strings, and booleans in Dart.

Now consider an object in state:

7 collapsed lines
class User {
String name;
int age;
User({required this.name, required this.age});
}
final user = Signal(User(name: "Jane Doe", age: 25));

Technically, it is possible to change the contents of the object itself. This is called a mutation:

user.value.name = "John Doe";

However, although objects in Reactter state are technically mutable, you should treat them as if they were immutable—like numbers, booleans, and strings. Instead of mutating them, you should always replace them.

Treat state as read-only

In other words, you should treat any Dart object that you put into state as read-only.

This example holds an object in state to represent the user. The user’s name is changed from "Jane Doe" to "John Doe" when you click the button. But the user’s name stays the same.

main.dart
1
import 'package:flutter/material.dart';
2
import 'package:reactter/reactter.dart';
3
4
void main() {
5
runApp(MyApp());
6
}
7
8
class User {
9
String name;
10
int age;
11
12
User({required this.name, required this.age});
13
}
14
15
final user = Signal(User(name: "Jane Doe", age: 25));
16
17
class MyApp extends StatelessWidget {
18
@override
19
Widget build(BuildContext context) {
20
return MaterialApp(
21
home: Scaffold(
22
appBar: AppBar(title: Text("Inmutable state example")),
23
body: Center(
24
child: ReactterWatcher((){
25
return Column(
26
children: [
27
Text('Name: ${user.value.name}'),
28
Text('Age: ${user.value.age}'),
29
ElevatedButton(
30
onPressed: () {
31
user.value.name = "John Doe";
32
},
33
child: Text("Change Name"),
34
),
35
],
36
);
37
}),
38
),
39
),
40
);
41
}
42
}

The problem is with this bit of code.

24
user.value.name = "John Doe";

This code modifies the object assigned to a new name when the button is clicked but it doesn’t trigger a re-render because the object itself hasn’t changed. The name property of the object has changed, but the object itself hasn’t. And Reactter doesn’t know that the object has changed because it’s still the same object. While mutating state can work in some cases, we don’t recommend it. You should treat the state value you have access as read-only.

To actually trigger a re-render in this case, create a new object and pass it to the state setting function:

24
user.value = User(name: "John Doe", age: user.value.age);

When value is set to a new object, Reactter knows that the state has changed and triggers a re-render.

Copying objects

In the previous example, you created a new object with the same age and a different name. But what if you want to change the age and keep the name the same?. You can do that too:

user.value = User(name: user.value.name, age: 30);

This is a common pattern when working with objects in state. However, creating a new object every time you want to change a property can be cumbersome. To simplify this process, you can add the following method to the User class:

1
class User {
5 collapsed lines
2
final String name;
3
final int age;
4
5
const User({required this.name, required this.age});
6
7
User copyWith({String? name, int? age}) {
8
return User(
9
name: name ?? this.name,
10
age: age ?? this.age,
11
);
12
}
13
}

This method creates a new object with the same properties as the original object, except for the properties you specify. You can then use this method to create a new object with the same name and a different age:

user.value = user.value.copyWith(age: 30);

Recap

  • Treat objects in state as read-only.
  • When you need to update an object, create a new one or make a copy of the existing object, and then set the state to use this new or copied object.
  • Avoid mutating objects in state directly.
  • Create a new object and pass it to the state setting function to trigger a re-render.
  • Use the copyWith method to create a new object with the same properties as the original object, except for the properties you specify.