https://github.com/lumia431/reaction
A lightweight, header-only reactive programming framework leveraging modern C++20 features for building efficient dataflow applications.
https://github.com/lumia431/reaction
modern-cpp mvvm-framework reactive-framework template-meta-programming ui-dataflow
Last synced: 13 days ago
JSON representation
A lightweight, header-only reactive programming framework leveraging modern C++20 features for building efficient dataflow applications.
- Host: GitHub
- URL: https://github.com/lumia431/reaction
- Owner: lumia431
- License: mit
- Created: 2025-03-07T10:01:39.000Z (2 months ago)
- Default Branch: master
- Last Pushed: 2025-04-29T14:42:29.000Z (18 days ago)
- Last Synced: 2025-04-29T15:26:22.018Z (18 days ago)
- Topics: modern-cpp, mvvm-framework, reactive-framework, template-meta-programming, ui-dataflow
- Language: C++
- Homepage:
- Size: 549 KB
- Stars: 285
- Watchers: 5
- Forks: 12
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- trackawesomelist - Reaction (β317) - A lightweight, header-only reactive programming framework leveraging modern C++20 features for building efficient dataflow applications. \[MIT] (Recently Updated / [May 03, 2025](/content/2025/05/03/README.md))
README
# Reaction: Modern C++ Reactive Programming Framework
[](https://en.cppreference.com/w/cpp/20)
[](https://en.wikipedia.org/wiki/Header-only)
[](https://cmake.org)
[](https://reactivex.io)
[](https://en.cppreference.com/w/cpp/language/templates)
[](https://en.wikipedia.org/wiki/Modelβviewβviewmodel)Reaction is a blazing-fast, modern C++20 header-only reactive framework that brings React/Vue-style dataflow to native C++ β perfect for UI, game logic, and more.
### π― **Focused on UI Dataflow Management**
- **Pure Data-Driven Updates** β Optimized for **one-way binding** (Model β View)
- **No Event Emitters** β Changes propagate **only through data dependencies**, avoiding callback hell
- **Predictable Updates** β Strict **top-down dataflow** like React/Vue, but with zero runtime overhead#### **Ideal For:**
β **MVVM/MVC UI Architectures**
β **Game Object Properties**
β **Form Validation Chains**
β **Animation State Machines**### π Performance Optimized
- **Zero-cost abstractions** through template metaprogramming
- Minimal runtime overhead with **smart change propagation**
- Propagation efficiency **at the level of millions per second**### π Intelligent Dependency Management
- Automatic **DAG detection** and cycle prevention
- Fine-grained **change propagation control**
- Configurable **caching strategies**### π‘οΈ Safety Guarantees
- Compile-time **type checking** with C++20 concepts
- Safe **value semantics** throughout the framework
- Framework manages object lifetime internally### π§© Extensible Design
| Feature | Options |
|------------------|----------------------------------|
| Trigger Policy | ValueChange, Threshold, Timer, Custom |
| Invalidation | Direct, KeepCalculate, LastValue |### π¦ Requirements
- **Compiler**: C++20 compatible (GCC 10+, Clang 12+, MSVC 19.30+)
- **Build System**: CMake 3.15+## π Installation
To build and install the `reaction` reactive framework, follow the steps below:
```bash
git clone https://github.com/lumia431/reaction.git && cd reaction
cmake -B build
cmake --build build/
cmake --install build/ --prefix /your/install/path
```After installation, you can include and link against reaction in your own CMake-based project:
```cmake
find_package(reaction REQUIRED)
target_link_libraries(your_target PRIVATE reaction)
```### π Quick Start
```cpp
#include
#include
#include
#includeint main() {
using namespace reaction;// 1. Reactive variables for stock prices
auto buyPrice = var(100.0); // Price at which stock was bought
auto currentPrice = var(105.0); // Current market price// 2. Use 'calc' to compute profit or loss amount
auto profit = calc([&]() {
return currentPrice() - buyPrice();
});// 3. Use 'expr' to compute percentage gain/loss
auto profitPercent = expr(std::abs(currentPrice - buyPrice) / buyPrice * 100);// 4. Use 'action' to print the log whenever values change
auto logger = action([&]() {
std::cout << std::fixed << std::setprecision(2);
std::cout << "[Stock Update] Current Price: $" << currentPrice()
<< ", Profit: $" << profit()
<< " (" << profitPercent() << "%)" << std::endl;
});// Simulate price changes
currentPrice.value(110.0).value(95.0); // Stock price increases
*buyPrice = 90.0; // Buy price adjustedreturn 0;
}
```### π Basic Usage
#### 1. Reactive Variables: `var`
Define reactive state variables with `var`.
```cpp
auto a = reaction::var(1); // int variable
auto b = reaction::var(3.14); // double variable
```- Method-style get value:
```cpp
auto val = a.get();
```- brief way:
```cpp
auto val = a();
```- Method-style assignment:
```cpp
a.value(2);
```- Pointer-style assignment:
```cpp
*a = 2;
```#### 2. Derived Computation: calc
Use **calc** to create reactive computations based on one or more var instances.
- Lambda Capture Style:
```cpp
auto a = reaction::var(1);
auto b = reaction::var(3.14);
auto sum = reaction::calc([=]() {
return a() + b(); // Retrieve current values using a() and b()
});
```- Parameter Binding Style (High Performance):
```cpp
auto ds = reaction::calc([](auto aa, auto bb) {
return std::to_string(aa) + std::to_string(bb);
}, a, b); // Dependencies: a and b
```#### 3. Declarative Expression: expr
expr provides a clean and concise syntax to declare reactive expressions. The result automatically updates when any dependent variable changes.
```cpp
auto a = reaction::var(1);
auto b = reaction::var(2);
auto result = reaction::expr(a + b * 3); // result updates automatically when 'a' or 'b' change
```#### 4. Reactive Side Effects: action
Register actions to perform side effects whenever the observed variables change.
```cpp
int val = 10;
auto a = reaction::var(1);
auto dds = reaction::action([&val]() {
val = a();
});
```Ofcourse, to get high performance can use Parameter Binding Style.
```cpp
int val = 10;
auto a = reaction::var(1);
auto dds = reaction::action([&val](auto aa) {
val = aa;
}, a);
```#### 5. Reactive Struct Fields: `Field`
For complex types with reactive fields allow you to define struct-like variables whose members are individually reactive.
Here's an example of a `PersonField` class:
```cpp
class PersonField : public reaction::FieldBase {
public:
PersonField(std::string name, int age):
m_name(reaction::field(this, name)),
m_age(reaction::field(this, age)){}std::string getName() const { return m_name.get(); }
void setName(const std::string &name) { *m_name = name; }
int getAge() const { return m_age.get(); }
void setAge(int age) { *m_age = age; }private:
reaction::Field m_name;
reaction::Field m_age;
};auto p = reaction::var(PersonField{"Jack", 18});
auto action = reaction::action(
[]() {
std::cout << "Action Trigger , name = " << p().getName() << " age = " << p().getAge() << '\n';
});p->setName("Jackson"); // Action Trigger
p->setAge(28); // Action Trigger
```#### 6. Copy and move semantics support
```cpp
auto a = reaction::var(1);
auto b = reaction::var(3.14);
auto ds = reaction::calc([]() { return a() + b(); });
auto ds_copy = ds;
auto ds_move = std::move(ds);
EXPECT_FALSE(static_cast(ds));
```#### 7. Resetting Nodes and Dependencies
The reaction framework allows you to **reset a computation node** by replacing its computation function.
This mechanism is useful when the result needs to be recalculated using a different logic or different dependencies after the node has been initially created.``Note:`` **The return value type cannot be changed**
Below is an example that demonstrates the reset functionality:
```cpp
TEST(TestReset, ReactionTest) {
auto a = reaction::var(1);
auto b = reaction::var(std::string{"2"});
auto ds = reaction::calc([]() { return std::to_string(a()); });
auto ret = ds.set([=]() { return b() + "set"; });
EXPECT_EQ(ret, reaction::ReactionError::NoErr);ret = ds.set([=]() { return a(); });
EXPECT_EQ(ret, reaction::ReactionError::ReturnTypeErr);ret = ds.set([=]() { return ds(); });
EXPECT_EQ(ret, reaction::ReactionError::CycleDepErr);
}
```#### 8. Trigger Mode
The `reaction` framework supports various triggering mode to control when reactive computations are re-evaluated. This example demonstrates three mode:
1. **Value Change Trigger:** The reactive computation is triggered only when the underlying value actually changes.
2. **Threshold Trigger:** The reactive computation is triggered when the value crosses a specified threshold.
3. **Always Trigger:** (Not explicitly shown in this example) Always triggers regardless of whether the value has changed.The trigger Mode can be specified by the type parameter
```cpp
using namespace reaction;
auto stockPrice = var(100.0);
auto profit = expr(stockPrice() - 100.0);
auto assignAction = action([=]() { // defalut AlwaysTrigger
std::cout << "Checky assign, price = " << stockPrice() <<'\n';
});
auto sellAction = action([=]() {
std::cout << "It's time to sell, profit = " << profit() <<'\n';
});
sellAction.setThreshold([=]() {
return profit() > 5.0;
});
*stockPrice = 100.0; // assignAction trigger
*stockPrice = 101.0; // assignAction, profit trigger
*stockPrice = 106.0; // all trigger```
You can even define a trigger mode yourself in your code, just include the **checkTrigger** method:
```cpp
struct MyTrigger {
bool checkTrigger() {
// do something
return true;
}
};
auto a = var(1);
auto b = expr(a + 1);
```#### 9. Invalid Strategies
In the `reaction` framework, all data sources **obtained by users are actually in the form of weak references**, and their actual memory is managed **in the observer map**.
Users can manually call the **close** method, so that all dependent data sources will also be closed.```cpp
auto a = reaction::var(1);
auto b = reaction::var(2);
auto dsA = reaction::calc([=]() { return a(); });
auto dsB = reaction::calc([=]() { return dsA() + b(); });
dsA.close(); //dsB will automatically close, cause dsB dependents dsA.
EXPECT_FALSE(static_cast(dsA));
EXPECT_FALSE(static_cast(dsB));
```However, for scenarios where the lifecycle of a weak reference acquired by user ends, the `reaction` framework makes several strategy for different scenarios.
- **DirectCloseStrategy:**
The node is immediately closed (made invalid) when any of its dependencies become invalid.- **KeepCalcStrategy:**
The node continues to recalculate, its dependencies work normally.- **LastValStrategy:**
The node retains the last valid, its dependencies use the value to calculate.Below is a concise example that illustrates all three strategies:
```cpp
{
auto a = var(1);
auto b = calc([]() { return a(); });
{
auto temp = calc([]() { return a(); }); // default is DirectCloseStrategy
b.set([]() { return temp(); });
}
// temp lifecycle ends, b will end too.
EXPECT_FALSE(static_cast(b));
}
{
auto a = var(1);
auto b = calc([]() { return a(); });
{
auto temp = calc([]() { return a(); }); // default is DirectFailureStrategy
b.set([]() { return temp(); });
}
// temp lifecycle ends, b not be influenced.
EXPECT_TRUE(static_cast(b));
EXPECT_EQ(b.get(), 1);
a.value(2);
EXPECT_EQ(b.get(), 2);
}
{
auto a = var(1);
auto b = calc([]() { return a(); });
{
auto temp = calc([]() { return a(); }); // default is DirectFailureStrategy
b.set([]() { return temp(); });
}
// temp lifecycle ends, b use its last val to calculate.
EXPECT_TRUE(static_cast(b));
EXPECT_EQ(b.get(), 1);
a.value(2);
EXPECT_EQ(b.get(), 1);
}
```Likewise, you can define a strategy yourself in your code, just include the **handleInvalid** method:
```cpp
struct MyStrategy {
void handleInvalid() {
std::cout << "Invalid" << std::endl;
}
};
auto a = var(1);
auto b = expr(a + 1);
```## **Contributions Welcome!**
We welcome all forms of contributions to make **Reaction** even better:
### **How to Contribute**
1. **Report Issues**
π Found a bug? [Open an Issue](https://github.com/lumia431/reaction/issues) with detailed reproduction steps.2. **Suggest Features**
π‘ Have an idea? Propose new features through GitHub Discussions.3. **Submit Pull Requests**
π©π» Follow our workflow:
```bash
git clone https://github.com/lumia431/reaction.git
cd reaction
# Create a feature branch (feat/xxx or fix/xxx)
# Submit PR against `dev` branch