DEV Community

loading...
Cover image for Eat Bear In Parts

Eat Bear In Parts

thekashey profile image Anton Korzunov Updated on ・16 min read

Year Bears in Part === one step at a time

I’ve started writing this article some time ago, and by an accident - just wanted to share one simple concept. I’ve called new born article as “Make Redux Great Again”, and it’s still partially about it - making something better and as it should be.

I was writing this article and one thing here, or another thing there, were requiring some “clarification”. By explaining nuances and used terms, or providing some examples, I’ve made this article much bigger than I’ve ever planned. I’ve created a big 🐻bear🐻, even if the idea was to ~fight~ eat it.

The article started as simple advice, and still the main idea behind it is an advice how to keep things simple. Not a simple big advice, but constructed from simple advices.

You know - you could not solve Big Problem - Big Problems are too big to be solved. Perhaps you should solve smaller problems - Divide and Conquer the bigger ones, or apply pattern known as “greedy”.

A greedy algorithm is an algorithmic paradigm that follows the problem solving heuristic of making the locally optimal choice at each stage with the intent of finding a global optimum... but nonetheless a greedy heuristic may yield locally optimal solutions that approximate a globally optimal solution in a reasonable amount of time. (thank you Wikipedia)

In other words - 🐻Eat Bear In Parts🐻.


Redux... I don't know is it a good word for you, or, you know, "r-word"... Redux was a great thing, Redux made a few revolution, Redux considered harmful, you don't need Redux. Strong opinions, weakly held.

People often complain - Redux brings the pain, not the joy. They regret ever using it, and ready to ditch it as soon as possible.

...but let’s double checks a few things.

And this article would not be about Redux, but about the "Way of Redux". The way you have to think to, let's call it, be happy with it.

Selectors

Redux without selectors is very slow, and with selectors it’s not much better, as long as our selectors are not always properly defined(be honest with yourself). It’s just not easy to create perfect selectors, especially with libraries and patterns we have.

Without proper selectors, proper level of memoization would not be reached, and Redux would cause a disaster of false positive update propagations. That's how it's designed to work, and that's not a Redux problem - any "single source event propagation" thingy (I am looking at you, React Context) is a victim.

The problem is not with memoization itself, but with "you" – with your expectations(you hope its working out of the box), and expectations you have to match by yourself(Redux user would use Redux library in a proper way).

Redux did a little mistake. It should call mapStateToProps twice and compare results. If results are not matching each other, then mapStateToProps is not as pure as it expected, and that was you who did a mistake. JFYI React is doing exactly the same for hooks, safeguaring you.

Usually - this the important part - checking the quality (or existence) of memoization, is, well..., skipped. Or developers are failing to fix some edge cases and dropping the ball on it. Outcome is not awesome - you, and your users are not happy.

The common memoization problems are:

  • single cache line. Near all memoization libraries stores only last result, which is (even it is the right decision) not quite compatible with the "component approach".
  • API. For example one powerful feature of reselect - memoization cascades - requires all selectors to have the same interface. Well - that makes everything dirty... and broken. In this case the only common interface is props - you have to pass an objects filled with everything as a second parameter, while it should be a todoId of type number, for example. This is not only a some sort of abstraction leaking, but just would not work by default, without the change of attribute comparison function (default is shallow equal). You may read more about it at Mark Erikson's Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance, but, unfortunately - there are no answers there.

But there are dozen other ways to "fix" memoization you should be aware of:

But you should understand one thing about selectors - they are not only for memoization, memoization here is just an implementation detail. Selectors are here to abstract UI from the data. In the redux-starter-kit or Dan Abramov's "Idiomatic Redux" video series you are even expected to colocate reducers and selectors in slices.

Selectors are here to keep your UI components simple, even if your data is not.

UI-connect-Store. Easy as 1-2-3.

But people forgot about it.

Data

The second important moment about selectors is what they are returning - the data, and that data is a source of memoization problems - you have to always return the same result.

We just had a conversation about memoization, and this time I'd like to ask another question- "do you need that data"?

A simple example - Todo List - what does it need as an input? Array of todos? Array of something? Just length of that array?

"It depends"(The best answer ever, btw):

  • in realty, many "containers" requires only length to decide what they are going to render - ListOfElements+Pagination, or NothingFoundComponent.
  • ListOfElements, which could be twice smaller that TodoList, might require ids of elements to render, basically to use them as keys to make render more efficient.
  • Does something requires array of Todos? Nope, nothing. Except Todo itself.

That's a mistake almost everybody was and is doing - providing a complex objects, leaking underlaying structures (and abstractions) to the UI when it's not required.

Here is a code to think about

const TodoHeader = () => (
 <section>
   <button>Add todo</button> 
 <section>
);

const TodoFooter = ({count}) => (
 <section>
   There is {count} todos found, have a great day!
 <section>
);

const TodoList = ({todos}) => (
 <>
  <TodoHeader />
  {todos.map(todo => <Todo key={todo.id} action={todo.action} />)}
  <TodoFooter count={todos.length} />
 </> 
);

<TodoList todos={[allMyTodos]} />
Enter fullscreen mode Exit fullscreen mode

Looking good, and absolutely standard. I reckon that's how your components are look like.

But what if we would change just one line?

const TodoList = ({count, children}) => (
 <>
  <TodoHeader />
  {children} // this line
  <TodoFooter count={count} />
 </> 
);

<TodoList todoCount={allMyTodos.length}>
 {allMyTodos.map(todo => <Todo key={todo.id} action={todo.action} />)}
</TodoList> 
Enter fullscreen mode Exit fullscreen mode

Is this simpler? Is it better? I've create two components from one, and I probably should explain myself - "Why do I made simple code more complex?".

This is actually "Inversion of Control"(IoT), where you are "returning control back to the user of your component to implement and customise it how they see fit". Think about it - children inside TodoList are defined in the absolutely different place. You don't have to perform a prop drilling to provide extra props for the component inside anther component, as long as that component 🙀is outside🙀. Found it here, you may read more about prop drilling here.

Well, the right question here would be When to break up a component into multiple components. And I am referring to the Kent C. Dodds article, which, again, is not providing any feasible answer.

Breaking components is a very opinionated process - sometimes it's easier to understand coarse ones, but sometimes more "sounds and sharp" are more handy. It's not about personal preferences, it's about the size of a "picture" you are able to have in your mind. And, well, human brain is not quite good at it.

But I've got at least 3 good answers for you:

  • the first one - split then it IS more than one component. How to detect it? Well, multiple return points is a sweet spot.

however, this advice is not quite applicable for Class Components, where you might have many class methods, doing their small jobs to assemble result for the one big render function. That is the same multiple components - they are even looking as the same small functions - just able to use one this.props, and one this.state shared for all.

  • the second, and just the great one - hooks. Hooks have got a simple rule - rule of hooks - when you can't change their execution order. And how much you may create with a straingforwand logic? In clear mind - not much. Hooks are encouraging you to keep things in a sound boundaries, in a safe place, where you will not bypass their laws. Hook based components have almost natural limit of complexity.

  • and the the last, third, answer is data - if different parts of your component requires different data, or data they need is derided from another data, thus they actually require something different - then probably that's a few different components merged together. How so?

Let's recall code we had:

// That's not a "list" - it has Menu at the Header
const TodoContainer = ({count, children}) => (
 <>
  <TodoHeader />
  {children}
  <TodoFooter count={count} />
 </> 
);

const TodoList = ({todos}) => (
 todos.map(todo => <Todo key={todo.id} action={todo.action} />
)

// we are "deriving" `todoCount` from `allMyTodos.length`
const Todos = ({ todos }) => (
  <TodoContainer todoCount={todos.length}>
   <TodoList todos={todos} />
  </TodoContainer > 
)
Enter fullscreen mode Exit fullscreen mode

Now, let's make a next step, and refactor this code a bit:

// just add one more prop - `displayCount`
// "derived" props are "derived" elsewhere, not here.
- const Todos = ({ todos }) => (
+ const Todos = ({ displayCount, todos }) => (
  <TodoContainer todoCount={displayCount}>
    <TodoList todos={todos} />
  </TodoContainer > 
);

export default connect(
 state => ({
   displayCount: state.todos.length, // imagine a selector
   todos: state.todos, // imagine a selector
}))(Todos)
Enter fullscreen mode Exit fullscreen mode

Now every piece consumes only what it need, we are not mixing things, and get more control upon our UI.

It might be not so clear why displayCount should be extracted as a separate props, so could answer one quick question - what would happen if todo.length would reach 10000? I hope - pagination. How you will implement pagination, or, to be more concrete, where - on the Todos level or TodoList level? In the second case todo.length would still reflect the "right" value, but in the first it would not.

Which case you would pick? Sorry, but I don't believe in any far-future predictions, and thus another question - which case would be more unpredictable future proof? Well - where displayCount is not bound to todos. This case is more optimised for change.

What if you will implement pagination on the server side? You will get N todos from one endpoint, like /todos/get/:page, and the total number from something like /todos/stat. 😉 "optimised for change"

But let's pick another example, to try this problem from a bit different prospective - let's fetch some data on component mount.

const Component = ({page, pageData}) => {
  useEffect(() => 
    dispatchDataLoading(page.id);
  }, [page.id]

  return <Page>{pageData}</Page>
}

// connect this Component somehow
Enter fullscreen mode Exit fullscreen mode

Here we are reacting on page.id value, dispatching some event to fetch the data we need, and eventually rendering this component with data fetched.
And both times we are doing twice more stuff that we need - trying to render data which does not exists anymore, or trying to load something we don't need yet.

In short - that are two different components - a View Component, and a "Sibling Effect" one.

const SiblingEffectComponent = ({page}) => {
  useEffect(() => 
    dispatchDataLoading(page.id);
  }, [page.id]

}

const ViewComponent = ({pageData}) => {
  return <Page>{pageData}</Page>
}

const Component = ({page, pageData}) => (
  <>
   <SiblingEffectComponent page={page}/>
   <ViewComponent data={data} />
  </>
);
// connect this Component somehow
Enter fullscreen mode Exit fullscreen mode

This refactoring has also another interesting effeect - now you may execute effect in the Sibling before rendering View - they are no longer bound to each other. However it still would be the same "render cycle".
Data update propagations are also separated, and you now might wrap both components with React.memo and this time it would work, as long data updates are also separated.

Moving some side effect aside - is an easy and very powerful pattern to tackle performance issues, making different parts of your application respond only to the stuff they need. Siblings form the family.

To keep UI simple - keep the data simple. It might require to create twice more selectors, but... but would it help?

Storybook

Many times I heard complains about Redux and Storybook. Like it’s hard to provide properly mocked Store for the story. Or, if not to provide, then to maintain. True story!

While having big "end-to-end" stories, with a real State underneath, is good to have - you don’t have to use (ok you can’t use) that sort of stories for everything. The problem is not with redux or components - the problem is with complexity - there are too many moving pieces.

Let's imagine you are doing everything right. Would we have any problems with a Storybook?

  • Develop UI components as UI components. You know - Stateless Dumb Components. And not only Stateless, but "Simple".
  • connect these UI components to the underpaying Store using selectors, which would play a role of an abstraction layer from a classical 3-tier system.

👨‍🔬 UI-connect-Store. Easy as 1-2-3.

  • if something changes in the underlying layer(Store) - you have to change only selector.
  • if something changes in the UI later - you have to change only selector.
  • Please keep layers separated, that would help you a lot.

Then - it would be easier to provide a right State, as long as your UI components are fully abstracted and simple.

But, you know, the first connected component inside your story is a party pooper. It will try to connect to the Store, read something from it, and eventually would fail. Or, at least, would not represent the "State" you need. Because there is no real store_ in the "static" storybook, providing right states for the different stories just a not yet solved question (in general).

Imagine a selector. All UI components consumes simple types, and controlling selectors (who said mocking?) you may control everything. You may control selectors, mocking is available in storybook, but is it the right way?

In any way - storybook is still a problem, and nothing above would help with it.
What if it would be better to handle this problem from another point of view?

Dependency Injection

The yet unsolved problem is "keep components simple", and "there are too many moving pieces". I mean - you might make your UI part simpler by making it simpler, and as UI would grow up - all the simplicity would disappear. Things could be simple, but only while they are small - big big simple things are even worse than big(not so big, but the way) and complex.

Solution - make smaller components from bigger ones. Just like we already did with todo list.

But solution we created is too, let's call it,- tangled. Let's detangle it!

  • yet again - this is our original todos. That's a single component.
const Todos = ({ displayCount, todos }) => (
  <TodoContainer todoCount={displayCount}>
    <TodoList todos={todos} />
  </TodoContainer > 
);

export default connect(
 state => ({
   displayCount: state.todos.length, // imagine a selector
   todos: state.todos, // imagine a selector
}))(Todos)
Enter fullscreen mode Exit fullscreen mode
  • And here is magic... it's still the single component, but scattered in space.
// removing "tangled" TodoList from here, replacing it by {todoList}
const Todos = ({ displayCount, todoList }) => (
  <TodoContainer todoCount={displayCount}>
    {todoList} // or just "children"
  </TodoContainer > 
);

export default connect(
// map state to props
 state => ({
   displayCount: state.todos.length, // imagine a selector
   todos: state.todos, // imagine a selector
}), 
// map dispatch to props
null,
// merge props
(stateProps) => ({
  displayCount: stateProps.displayCount, // keep this prop
  todoList: <TodoList todos={todos} />, // WHAT???
});
)(Todos)
Enter fullscreen mode Exit fullscreen mode

Magic - we just injected internals to our TodoContainer from connect. That's a pure dependency injection pattern, but in a component form.

You might ask - OK, SO WHAT DOES THIS CHANGE? (except Inversion Of Control we talked before)

Finite components

This changes you ability to control components. This makes them "finite". No component would render something inside it, "till the very end", as long as those infinite internals are provided as props.

Every "next" step is provided as a slot(children) prop. Every component without slots provided is finite - it would render just nothing. It would be compact and simple, as it was expected to be all this time.

This makes (unit) testing simpler, developing simpler, storybooking simpler, gives you absolutely different reusability level, and everything you need - is to properly wire components.

PS: And think about BEM architecture. It may sounds very interesting from now. What does it mean to be Absolutely Independent Block (the first name given to the BEM technology)

Does it heal the Storybook?

Well - imagine you have a story for ComponentA. For the storybook purposes you are providing everything you need as a props, or even wrapping your component with Provider, providing the "right" state.

You have your StorybookedComponentA.

Now imagine you have a story for ComponentB, and it uses ComponentA somewhere inside.

  • In the real application you will provide it as a componentA slot from connect function, just like in the example above.
  • In the storybook instead of a "real component", you may provide a story for ComponentA, ie StorybookedComponentA, thus you may construct one story from another story 🤓.

Again - story is a some "situation" you may control a bigger situation from a smaller ones,

And new Storybook Component Format would help you with it:

Just eat bear in parts and it would be eaten!

Fire Wire

And now we are getting close to the conclusion - to the Smart/Dump separation of concerns, which is a real "s-word" nowadays.

Separation of Concerns gives you:

  • the ability to develop feature(s) in a parallel. One kind of developers(UI) would create, well, UI, and state management would be accomplished by another kind. These tasks requires different skills, believe me, so it's better to use developers with different specialisations to achieve better results.
  • easier way to change UI and underlaying state management - they are not bound to each other.
  • better rules when to split and create components, better boundaries for the global state and local state. If you were given a task to "create an UI" - you will clearly understand which variables should be contained inside your solution, and which one should be external.
  • lower mistake rate. And even in case of a mistake - it's easier to handle it. Everything is small, decoupled and isolated like... like hooks.

As long we are talking about Redux - we are talking about connect function, which is not "fancy". And reselect is not "fancy". Only the Hooks are fancy!

Would hooks save the day?

v7

Redux v7 is a game changer. It gives you hooks. Honestly - for the first time I was against it - I want to have as less logic in my "React" components, as possible. But then, you know, I get used to it.

useCallback, useMemo - and other stuff you are not able to use - that's beautiful!

const Todos = ({ displayCount, todoList }) => (
  <TodoContainer todoCount={displayCount}>
    {todoList} // or just "children"
  </TodoContainer > 
);

// the same `connect`, this time with `useMemo`
// and, the main, in a "common" form.
const TodoConnected = () => {
  const displayCount = useSelector(state => state.todos.length);
  const todos = useSelector(state => state.todos);
  const todoList = useMemo(() => <TodoList todos={todos} />, [todos])

  return <Todos displayCount={displayCount} todoList={todoList} />
}
Enter fullscreen mode Exit fullscreen mode

But there is one problem - reselect is not hooks compatible. Any memoization cascade, and reselect is not as good without this pattern, are not compatible with hooks laws - as long as they might have different parts of one selector differently memoized (they might have the memoized, that's already enough) - they would violate the Rules of Hooks.

Any memoization cascade, and reselect is not as good without this pattern, are not compatible with hooks laws

There are ways to mitigate this problem, like redux-views or useReselect, but again - they are fighting about implementation.

Simpler solution is simpler that you might think - just replace v7's useSelector buy useTrackedSelector, which will let you skip any memoization problems possible.

Honestly - I would not recommend using hook based memoized selectors as react-redux it proposes - again it struggles from reselect limitations.

Usage tracking has it's own limitations.

4th answer to a component splitting

Usage tracking has it's own limitations. It just tracks which variables you are reading from the state, not knowing why. It's uses a few quite logical assumptions, which are ... they only one we have:

  • if you read props.a - you need props.a
  • if you read props.a.b - you need props.a.b, but you don't "need" props.a - it's a a "holder" for the real variable you need. Like state is just a "holder". The assumption is - you need only the deepest, the "real", variables, but not intermediate ones.
  • so if you really need values of props.a and props.a.b simultaneously - you have to separate components, or isolate these reads in selectors.

In the following example update would not be triggered on state.todos change, if state.todos.length would keep the old value.

const Component = () => {
  const state = useTrackedState(context);

  return (
    <div>
     I have {state.todos.length} things todo:
     <TodoList todos={state.todos} />
    </div>
  )
Enter fullscreen mode Exit fullscreen mode

Solutions: split components, isolate reads, or "derive props in other place" (we did it before)

And yet again - you have to understand "boundaries" you have in your component, and separate these concerns - or magic would not work.

Here is some theory behind:

Nowadays it's better to rely on automatic usage tracking rather on your own skills. Or use MobX. And I am serious.

--

Redux of Not?

Another big question to answer is where to use Redux, and where hooks, Context, and friends should be preferred.

Again - this is not the right question. The right question should be, yet again, be about boundaries.

When one state management system should end, and another state management system begin.

It's not about the boundary point between Redux and Local State - it could be a point between One Redux and Another Redux. This is more about fractal state, and microfrontends than Redux itself. But not isolation.

Think about how your application is build.

  • part of your application is built from the group up - Buttons(Atoms), Buttons forms Forms(Molecules), and Pages (organisms). Usually lower pieces does not have any state and are controlled by they owner. Owner is doing the same. This was you are building your "reusable" elements, which just obey, and does not have any "own" opinion.
  • there is also "idea" of your app, and the main data - "THE STATE" - to fulfil it. This state lives on top, and flows down. It, and components working with "it" do have their "own" opinion.
  • there are complex situation in the middle, when "local" state variable might be required later in the saga or other middleware. Sometimes you might just send it as a part your "action", but sometimes it's easier to "synchronise" local and global variables.

Really, if it's okey to control input's value and update the local state onChange – why it's not ok to do the same in redux terms?
Well, many form management libraries are doing exactly the same.
PS: Reverse synchonization is also not a big problem.
PPS: Single source of truth? Not a problem with uni-directional synchronisation.

SO?!

So I would amend this article with more examples, and by the time it would become better and better.

There here is some advices I could give you:

  • try to "see" the complexity, and then try to avoid it. If something seems to be not very complex for you - it might be very complex for another person, including a tomorrow you.
  • try to use POD(plain old data) types - no complex structures should penetrate your application. There are still some place for "complex" objects, if some data(a thing) should be complex, but try to derive simpler (and more sound) values from that data to be consumed by target UI or logic.
  • "call the things by their names". If you need to display how much todos you have - that's todoCount, not todos.length.
  • use IoT pattern to remove complexity from component composition. Actually it IS component composition.
  • try keep UI, state management and data access separated.
  • Divide And Conqueror. Every time problem become to big to be solved - break and decompose it.
  • in the same time - avoid introducing senseless small pieces, which may lead to spaghetti code, or just not fit all together in your head.
  • try to think in Things. In Domains. In Hierarchy and Requirements (well here is more about it).
  • try to keep everything under control.

But you should already understand that the only way to eat your bear - is to 🐻Eat Beat In Parts🐻.

Discussion (0)

Forem Open with the Forem app