Came ready to argue, left nodding my head. Good content
@UIEngineering
Ай бұрын
😂 Good one xd
@hosono3918
Ай бұрын
I never considered using a ref to modify a components state from a parent. It looks like a great pattern to add to the toolbox. I usually just use a context or a "function as child component" to do this, but the imperative handle approach seems way simpler. Great video, as always.
@brunokawka
Ай бұрын
3:05 I was going to comment why don’t you leverage useImperativeHandle which serves the same purpose, but thankfully watched til the end :) I find it especially useful for components that orchestrate some complex animations (populating start/pause/stop methods for example), or need to share a friendly api to interact with internal state, where the controlled state (lifting it up) wouldn’t make sense from the design/performance perspective.
@UIEngineering
Ай бұрын
That's cool, good to know people already use this no matter what the docs say. I'm not sure if I understand the animation example, though. Seems like you're exposing DOM methods, which is what useImperativeHandle is made for, right? Still 100% valid, but I was focusing on exposing the setState method, since for some reason the docs try to convince us it's wrong. About the useEffect: I just really, really don't like the 'imperative' part in the useImperativeHandle name, so I wanted to show how it can be easily implemented with useEffect and it's not any magic. But yeah, useImperativeHandle is much more convenient to use👌
@JakeHaugen
Ай бұрын
I disagree with your phrasing of the problem. You are still "lifting the state up to the parent", it's just that you have to pass a ref to get around react's limitations. Not saying you're wrong, it is more performant, but notice that you still had to modify the parent in both cases. Theoretically this inefficiency could even be fixed by a compiler that is aware that the button just sets the state and never displays it, so you don't have to do this ref passing yourself. In my mind, you're still lifting the state up as a principle and just recognizing that the "easy" way to do it causes performance issues because it does unnecessary work, so you suggest going with a more "correct" way to pass the correct state to both components. Important performance point but it's still lifting state up because the parent is still aware of the connection between the two. In 99% of cases I'd tell a junior engineer to just do the easy thing though because it's more maintainable (notice you lack typesafety on your handlerRef.current object, what if toggleIsActive changes), easier to reason about for new engineers as it's explicitly taught in the docs, more likely to be optimized by react's compiler in the future, and still can be easily optimized manually as you mentioned with something like a useMemo. But do appreciate the idea of having each component declare their individual setting methods. However, I would still say a better patten for individual setter functions is just custom hooks.
@codeVictor
Ай бұрын
I use this pattern a lot. Especially when I have a timer component. I add the setinterval and state in the timer component and expose the current time through ref. That way, I can get the total time onSubmit without having to pay the cost of unnecessary re-renders.
@UIEngineering
Ай бұрын
Sounds interesting. If you need the time once for some event handling action I guess it makes sense, but I'd be careful with exposing state VALUE outside if it's needed for rendering in the parent component - than lifting up is the way to go I think.
@zzzuq
Ай бұрын
Solid as always, thanks!
@UIEngineering
Ай бұрын
🫶You bet!
@Nellak2011
Ай бұрын
I totally agree. Having state travel up the chain violates principles that make code easy to reason about. Ideally, you would have every component be a pure function that is completely determined by their props, except the root component which has the State and Services it aggregates and passes to the children. Doing it this way will ensure each child component is testable and completely determined by their props. It also ensures that you can switch out the State / Service providers for each component without having to change your code. --- Example: (Old way, bi-directional prop drilling) const parent = () => { const [isActive, setIsActive] = useState(false) const child = ({ onClick }) => { const [isActive, setIsActive] = useState(false) return ( {isActive && It is active} onClick(isActive)}>{isActive ? "Deactivate": "Activate"} ) } } ---- Example: (New way, uni-directional prop drilling, State / Service pattern) // Only root component is stateful, rest are pure functions completely determined by props const parent = () => { const [isActive, setIsActive] = useState(false) // Note: This is used in place of redux selectors + redux reducers/thunks // All the State is aggregated here and is agnostic of whether it is useState, context, or redux or anything // This becomes more apparent when you use Redux selectors.. const appState = { child: {isActive} // normally would be a redux selector } // All the Services are aggregated here as well. Normally would include reducers, Thunks, API calls, etc. const appServices = { child: {setIsActive} // normally would be a redux reducer/thunk } // Note: In a real application, you would only pass the state/services this component needs // Notice how child is pure and isolated and agnostic const child = ({ state = appState[child], services = appServices[child] }) => { const { isActive } = state const { setIsActive } = services return ( {isActive && It is active} setIsActive(old => !old)}>{isActive ? "Deactivate": "Activate"} ) } } ---------- The benefits of this State / Service pattern and uni-directional prop drilling are: 1. State / Service props are uniform so there is no guessing 2. The State and Services have a known shape 3. At a glance you can see what your component State and Services are without having to guess (Separation of Concerns) 4. There is loose coupling as you are coupled on only the names of the fields you destructure from the state and services (instead of tight coupling as before) 5. The children are completely determined by their props, which means that if the data is correct and you tested it renders properly given props, then the UI tests are simplified (Data Oriented approach) 6. There is a single source of truth for State and Services 7. There is one way of data flow, down-wards, which simplifies things 8. Re-usability. You can now extract child component and use it in a completely new project and it will work exactly as it did before. 9. Testability. You can view the component in isolation and know exactly how it will operate 10. Readability. This makes things easier to comprehend since it is uni-directional
@UIEngineering
Ай бұрын
I can relate to the sentiment, and having components as pure functions definitely makes things easier. Grouping all state together at a global level can seem like a good idea, but you're forgetting that in React, state is tightly coupled with re-renders. So your solution leads to a complete re-render of the entire app whenever any part of the state changes. I much prefer to keep state local, and that's what this pattern is about. I've also discussed the same concept in 'Senior-Level Understanding of React Portals.' I actually believe the opposite: having state as local as possible is beneficial in React because it makes React do less work. Interesting comment though and I appreciate the effort 🫶
@Gruby7C1h
Ай бұрын
Hmm, it never hurts to question status-quo but in my practice I haven't noticed performance issues caused by moving simple state up. For more sophisticated stuff I'm happy with Redux/RTK Query (yeah, it's a bit complex but state management and caching are complex problems).
@mattvicent
Ай бұрын
Only problem I see with this is when it comes to typescript. But it's solvable, you'd have to declare a type for the imperativeHandle return and then import it into the parent to type the ref. E.g.: type ActionsHandle = { open: () => void close: () => void } // in the parent import { ExampleComponent, type ActionsHandle } from "example-component.tsx" const Parent = () => { const actionsRef = useRef(null) return }
@hghmnds
Ай бұрын
Was just thinking about how to make it work on ts
@piaIy
Ай бұрын
There's a much simpler way that doesn't require any type imports: useRef(null)
@UIEngineering
Ай бұрын
Yup, I tend to omit TS entirely in the videos for better or worse. I just don't want to introduce any more complexity and make videos digestible. But yeah, fair point.
@Channelcustomization832
Ай бұрын
Agree! Such a good idea. To combat the same issur I have used state manager libs, that rerender the components that are observing the state change, similarly to your solution.
@UIEngineering
Ай бұрын
Hope this helps to simplify it a bit! But yeah, seems like we had a similar observation😁
@NickCarboneDrum
Ай бұрын
Awesome! 🎉🎉🎉
@UIEngineering
Ай бұрын
Thank you! Cheers! 🫶
@pokefreak2112
Ай бұрын
I agree, but why spend this much effort trying to improve React? What's so good/special about it that justifies sticking to it despite these bizarre performance pitfalls? I switched back to vanilla js and I'm just as if not more productive with it.
@piaIy
Ай бұрын
Most of us work in teams on actual products that require reusable components and overall a rich ecosystem to deal with the problems we face every day, something that's very difficult if not impossible to achieve at scale with vanilla. And the performance is not as big of an issue as some might claim, especially now that the React compiler has been introduced.
@pokefreak2112
Ай бұрын
@@piaIy Vanilla _can_ scale well as long as you split your code into components like you would in a framework. I'm currently working on a monorepo consisting of several servers, browser games and admin panels and all the UI is defined using tailwind+ jsx compiled to real DOM nodes. The only thing React really gives you is the whole "updating state automatically rerenders the view" thing, and that's just not necessarily or even gets in the way for most kinds of applications. I use custom events and proxies to solve this problem, and the whole codebase only needs it a couple dozen times. And the vanilla js ecosystem is more than enough, most react libraries just wrap vanilla libraries or do something that would've been trivial to implement yourself in vanilla.
@zokizuan
24 күн бұрын
thanks
@minyoungna6642
Ай бұрын
Do you think React can evolve to have some sort of an effect handler that manages its own lifecycle ? I think the modal is a great example where the only real "control" that's exposed is the ability to conditionally render it. Maybe this is the one reason why people like SolidJs? Sorry for the noob question :D
@rakhman8621
Ай бұрын
Don’t you need 10 useRefs to control the panel in your example at 4:30? If so, what is the benefit in that case?
@aj35lightning
Ай бұрын
also wondering this. i get its more rendering efficient, but not sure if it also looks cleaner and more compartmentalized as the screenshot
@UIEngineering
Ай бұрын
You're right, and that's a great catch. TBH, my mistake - I should've shown the second example with 10 useRefs (or one big object in a ref) to make them equivalent. So I guess this argument is not so strong. Still, you only define it for setters, not the state itself, so it will involve a bit less code regarding definitions and less 'plumbing' on the props level. But good point!
@sarojregmi200
Ай бұрын
Gotta say, it's a great video.
@UIEngineering
Ай бұрын
Thanks bro, appreciate it! 🙇
@aghileslounis
Ай бұрын
Excellent video. Yes, software engineers should always use critical thinking instead of following without understanding the why. Do you think the React compiler will eliminate this problem of re-rendering, thus the pattern you described will not be necessary in terms of optimization?
@UIEngineering
Ай бұрын
Thank you much! Regarding Compiler - that's a very good question and honestly I don't have an answer for now. But I will have to check this definitely and I will get back to you. I have an expectation that it should solve the case, but I'm also wondering what will happen if you don't follow React Rules precisely etc. Thankfully it's still not only about performance (encapsulation argument from 3:39), but yeah, good point, I'll have to investigate this :)
@aghileslounis
Ай бұрын
@@UIEngineering Yeah we definitely need to try it first, but I know that they also developed an official ESLint plugin that will enforce you to use React the correct way in any situation.
@pieterdepauw6599
Ай бұрын
Disclaimer: I am not totally sure whether I am experienced enough to comment on this, but I tried my best to provide some feedback. I hope that I am using the right terminology, my apologies if that is not the case. I would argue that your pattern does not really conflict with the general advice to "Lift State Up". Essentially, you are not doing that all, because this use case does in fact not require you to lift up the state itself. The reason why that you don't need to lift the value of the state itself up is intrinsically tied to the fact that this pattern only working,r what is essentially a binary state ("toggleable state"). Because of that, you could say that, in some sense, the setState function is fulfilling both roles here: you can the state value to the opposing value can be done by calling the function, and the current state value really just follows from the number of times that you have called the setter function. However, this pattern is only possible if you presume that those specific conditions are met. However, from an educational viewpoint, I would argue that carving out an exemption to this general rule might add complexity and might lead developers towards some really bad patterns. Because you are basically "splitting" the state and state setter up while also potentially providing them to components that are both "UP" as well as "DOWN" the component tree, you depart from the expected unidirectional flow in the component tree, in which the state can only be set within or downstream the component in which the state is managed. I can't really judge adequately whether or not those risks are worth it or not.
@UIEngineering
Ай бұрын
Haha, appreciate the disclaimer 😇 I agree it’s not in conflict with the general advice; I was just arguing that the docs are not completely right, as they suggest exposing setState outside of the child component is wrong. I’m not sure if I understand a bit on “toggleable state,” because it looks like a part of your answer was cut in half. I definitely think this pattern can be applied to a much broader range of cases when you have all sorts of shapes of state. Just from the top of my mind, you could have an Input component that you want to be uncontrolled, and you might want to reset its value with exposed setState(). About the educational viewpoint, I’d argue the opposite. Every pattern can be misused if not understood correctly; that’s not an excuse to not discuss things. I’d agree that’s a bit more advanced topic, but it’s not that complex either. About the unidirectional data flow, I’m not sure. I get the argument, but on the other hand, the data flows in both directions anyway (when you pass a setState as a function to a child component and call it from the child, the data travels up). I know this pattern extends the limits further, but you can still trace all state changes to the exposed functions. It can make debugging a bit harder, but the pattern itself also has its upsides. There are always some costs for every solution, I guess, but yeah, it’s good you are pointing this out. Thanks for the comment 🙏
@lightyagami5963
Ай бұрын
Cannot agree more, but it's one part of the downside of React, because it's not smart enough to complete the accurate render process. In fine-grained reactivity system like Vue or Solid, it's totally OK to *Always* lift state up when possible, because by making your child component uncontrolled is actually giving more control ability to the parent component, or let's say container component. It's more about scalable and re-useable, especially when you have new requests come in to render some state as well, you can have the smallest change of your code without touching your child component.
@piaIy
Ай бұрын
Luckily, as always, the ingenuity of the React team comes to the rescue with the new compiler that fixes this problem and ensures that React remains the king of the FE frameworks 👍
@UIEngineering
Ай бұрын
@lightyagami5963 Yeah, fine-grained reactivity seems to be winning now and I definitely want to look more into it. @piaIy About the React Compiler, looks promising but it's still not a swiss army knife and it won't solve all performance issues with React. TBH out of these 2 concepts the fine-grained system seems more promising IMHO. Good comments though, thank you both!
@Metruzanca
Ай бұрын
All the gymnastics required to fix React. This is why I prefer Svelte or Solid.
@UIEngineering
Ай бұрын
No complex problems in Svelte and Solid then? Seriously curious 😁
@Its-InderjeetSinghGill
20 күн бұрын
Just use Jotai
@purpinkn
Ай бұрын
devs are so dumb today, they think react is worth more than the last dump i took after indian food.
@PooyaBadiee
Ай бұрын
React devs are just a bit too scared of using refs
@UIEngineering
Ай бұрын
Are they though? I've seen so many examples of overusing refs online, I guess that's not that bad 😅
Пікірлер: 50