Dan Abramov recently tweeted a nice perspective of “Separation of Concerns” with a lot of sub-tweets explaining it in details. I felt it will be good to extract the tweets into a medium story for the sake of the community and future reference.
You can find the original tweet here:
Separating concerns by files is as effective as separating school friendships by desks. Concerns are “separated” when there is no coupling: changing A wouldn’t break B. Increasing the distance without addressing the coupling only makes it easier to add bugs.
That’s not to say that separating things into files isn’t helpful. It lets you choose an appropriate balance between the granularity of navigation and the frequency of context switching. But your file structure has nothing to do with decreasing coupling, or separating concerns.
A rigid file structure may help introduce some desired inertia to prevent stronger coupling. If there’s a distance between A and B, it’s harder to couple them clearly. However, a future problem *might* require more coupling! Since it’s made difficult, it’s just done poorly.
Each problem is a multidimensional beast. The number of dimensions fluctuates over time as the requirements change and as your understanding evolves. You can’t expect all problems to fit into three (or any number) of predefined “dimensions”.
However, that’s what “frameworks” do. They trade the precision in describing problems for expressing a subset of them using the same independent “dimensions”. It’s really useful if many of your problems seem to have similar “shapes” (e.g. “an interactive thing on the screen”).
There are multiple ways to break this problem apart. For example: its initial tree structure + a cascade of possible appearances of anything + code that can manipulate the tree in response to interactions. That’s kind of classic HTML+CSS+JS. Then you realize that maybe “initial tree structure + code that manipulates the tree” isn’t a very helpful way to describe dynamic things. It’s like if you called a taxi, and the driver asked you for directions on every turn instead of the target address. So it might be more helpful to separate the problem differently: “tree structure at any given time + how to update the tree from the previous to the next structure”. The second subproblem is sufficiently generic that we can hide it in a library. We collapsed a problem dimension!
Of course this only works well if the problems solved with this “framework” can still be split into predetermined “dimensions” despite changing requirements. To find such “dimensions”, you can start with listing things that you expect to stay true despite any changes.
For example: UIs are usually not random. It’s often desired that the same “thing” (e.g. button) behaves and looks consistently throughout the app. If that wasn’t usually true, components wouldn’t be such a useful abstraction.
UIs also tend to be relatively “stable”. Most commonly, every piece of UI has a limited “repertoire” of things it can do and the ways it can look. This lets you reduce a problem of “how to fill 10k pixels” to “what are the 5 possible states, and how does interaction change them”.
Notice how animation in React is a bit difficult to think about? (Even though imperative libs like Animated offer a great escape hatch!) That’s because animation messes with the “stability” described above so it’s more challenging to express within the chosen “dimensions”.
Half of engineering is how to make the thing work well. Another half is how to ensure it keeps working well over time as it changes. That requires splitting problems in a way that isn’t just correct, but also accessible to our brains with very limited focus and short-term memory.
And because we’re so bad at programming, it’s important that we often re-evaluate when splitting a problem in a certain way is helpful, and when there’s a way that works better with problems of a certain shape.