performance, JSX - when I think about what’s special about react, it’s the way I can think about my views - your application data is passed in at the root - and the UI produced is a function of that data, that is, - with the same data as input, it will always produce the same output - when the data changes I just re-run the function and React will update the UI
<h1> Hola, ${data.name}! </h1> `; } document.body.innerHTML = render({ name: "JSConf UY" }); - For the purposes of this talk you can think about it as one giant template function - Every time the data changes, we re-render the template, - and just blow the old view away - This makes it much easier to reason about what’s happening in our view layer - Easier to bring on new people
React your data lives outside of this view hierarchy - I can now easily reason about my view layer - How can I structure my application so that it’s easy to reason about my data?
React your data lives outside of this view hierarchy - I can now easily reason about my view layer - How can I structure my application so that it’s easy to reason about my data?
like this Data completely separate from the view We know that when the data changes we can re-render our view So let’s add that functionality into our data layer and change the name
changed Views subscribe to the stores that contain the data that it needs Data updates, re-render the view, we know this stuff This tends to be pretty intuitive for frontend developers
some times - receives actions and passes them to every registered store - Every action passes through the dispatcher - Every action is passed through every store - It handles dependencies between stores, but today we don’t have to think about that
basically ignore the dispatcher and view layers - I want to focus on the interaction between actions and stores - Still abstract, let’s get a concrete example
$200 $200 Withdrawal ($50) $150 Deposit $100 $250 $250 And the balance is a measure of those interactions NOTE: If we preform the same transactions, same order, these results will be the same The balance is derived data In flux terms, the transactions on the left are our actions and the balance on the right is a value that we would track in a store
switch (action.type) { case Actions.WITHDREW_FROM_ACCOUNT: balance -‐= action.data.amount; break; case Actions.DEPOSITED_INTO_ACCOUNT: balance += action.data.amount; break; ... } } This would be inside a store that tracks account balance
switch (action.type) { case Actions.WITHDREW_FROM_ACCOUNT: balance -‐= action.data.amount; break; case Actions.DEPOSITED_INTO_ACCOUNT: balance += action.data.amount; break; ... } } The dispatcher makes sure that every action in the app invokes onDispatch
switch (action.type) { case Actions.WITHDREW_FROM_ACCOUNT: balance -‐= action.data.amount; break; case Actions.DEPOSITED_INTO_ACCOUNT: balance += action.data.amount; break; ... } } When we withdraw money, we decrement
switch (action.type) { case Actions.WITHDREW_FROM_ACCOUNT: balance -‐= action.data.amount; break; case Actions.DEPOSITED_INTO_ACCOUNT: balance += action.data.amount; break; ... } } And when we deposit money we increment After this method, the store emits a change, and the view re-renders
the way we generally think of them - It's tempting to think of stores as just models that live outside of your view hierarchy - but stores do not behave like the traditional models that we think of (O.o) - How so?
can update the value.. wait… - But there’s no equivalent for a setter - You can’t call up your bank and tell them that your balance is now one million dollars - Stores update in response to actions, but there’s no way to update just one value, - or just one store - ACTIONS become the ONLY WAY to MODIFY our state - There’s an important result of this
f(state, [...actions]) → newState - Given a set state, the transition to another state given a set of actions is deterministic. - If I fire the same sequence of actions in my app, I will end up with the exact same state - Source of truth is actually the stream of events - Stores are a “cache”
switch (action.type) { case Actions.WITHDRAWAL_REQUESTED: requestWithdrawal( action.data.accountId, action.data.amount ).then( res => balance -‐= res.amount; ); break; ... } } New Action Make a request, and when the response comes back, update the value The store updates with the correct value and the view will render correctly
switch (action.type) { case Actions.WITHDRAWAL_REQUESTED: requestWithdrawal( action.data.accountId, action.data.amount ).then( res => balance -‐= res.amount; ); break; ... } } But now there is a mutation of our data that’s not in this stream of actions If we re-apply our actions we end up in a different state If something else needed to know about the withdrawal, now it can’t Harder to reason about our app
.done( res => dispatch({ type: Actions.WITHDREW_FROM_ACCOUNT, data: { ... } }), err => dispatch({ type: Actions.WITHDRAWAL_FAILED, data: { ... } }); ); } You might do it this way, outside of the store
are a convenience - Given list of all transactions that I’ve ever made, can I afford to buy lunch? - This is what we used to have to do balancing a checkbook (ask your parents) - We decide what stores to have based on what questions we want to ask
not the only question we’ll need to ask of this data - In large systems many different subsystems may need to know about what’s happening - Because every action is passed to every store we create more stores
message: "Your withdrawal has failed", ... } } But this isn’t a good action SHOW_NOTIFICATION is a command, not “something that happened” Now, I have to sprinkle this action all around the application We’re trying to get around the lack of a setter and talk to a particular store
switch (action.type) { case Actions.WITHDRAWAL_FAILED: messages.push("Your withdrawal has failed"); break; case Actions.NOTIFICATION_DISMISSED: messages = []; break; ... } } Our view layer simply renders a notification for each value in messages Empty -> no notification When a withdrawal fails, messages now has a value view re-renders
switch (action.type) { case Actions.WITHDRAWAL_FAILED: messages.push("Your withdrawal has failed"); break; case Actions.NOTIFICATION_DISMISSED: messages = []; break; ... } } Your withdrawal has failed and we have a notification, when the user interacts with the view or a time limit is reached the dismiss action is fired and it’s not longer rendered Maintain separation of concerns. The code firing the action has no idea the notification system is listening.
Actions.WITHDRAWAL_REQUESTED Actions.WITHDRAWAL_FAILED Actions.DEPOSIT_REQUESTED Actions.DEPOSITED_INTO_ACCOUNT Actions.USER_CHANGED_PASSWORD Actions.USER_UPDATED_PHONE_NUMBER Actions.WITHDRAWAL_REQUESTED Actions.WITHDRAWAL_FAILED Actions.DEPOSIT_REQUESTED Actions.DEPOSITED_INTO_ACCOUNT Actions.USER_CHANGED_PASSWORD Actions.USER_UPDATED_PHONE_NUMBER Actions.WITHDRAWAL_REQUESTED Actions.WITHDRAWAL_FAILED Actions.DEPOSIT_REQUESTED Actions.DEPOSITED_INTO_ACCOUNT Actions.USER_CHANGED_PASSWORD - our app looks like this when it’s running - every action passes through the dispatcher - can log them all out - I use this at work to understand new sections of the UI that I haven’t worked on before
switch (action.type) { case Actions.WITHDREW_FROM_ACCOUNT: balance -‐= action.data.amount; break; case Actions.DEPOSITED_INTO_ACCOUNT: balance += action.data.amount; break; ... } } When looking at a store the actions that can modify it are explicit This is the exhaustive list This helps narrow the scope of what I need to understand in a large system, especially if we keep the stores small Make changes with confidence This allows us to keep moving fast, even as our systems get large
see that you now have -10 dollars as your balance - WHAT HAPPENED? - A user sends you a screenshot of your app in a weird state: I HAVE A BUG - This is the same situation - Repro please
$200 $200 Withdrawal ($50) $150 Deposit $100 $250 $250 If this is our bank account we have a history to look at If this is our app, we are missing most of this data
Actions.WITHDRAWAL_REQUESTED Actions.WITHDRAWAL_FAILED Actions.DEPOSIT_REQUESTED Actions.DEPOSITED_INTO_ACCOUNT Actions.USER_CHANGED_PASSWORD Actions.USER_UPDATED_PHONE_NUMBER Actions.WITHDRAWAL_REQUESTED Actions.WITHDRAWAL_FAILED Actions.DEPOSIT_REQUESTED Actions.DEPOSITED_INTO_ACCOUNT Actions.USER_CHANGED_PASSWORD Actions.USER_UPDATED_PHONE_NUMBER Actions.WITHDRAWAL_REQUESTED Actions.WITHDRAWAL_FAILED Actions.DEPOSIT_REQUESTED Actions.DEPOSITED_INTO_ACCOUNT Actions.USER_CHANGED_PASSWORD But we have exactly that! We just need to save them off
- But we can only do this because we make our mutations explicit and keep a history - So the next time someone sends you a screenshot of your app in a weird state…