Submind YouTube summaries
Thumbnail for EmberConf 2024 - Contextualizing State with Kevin Kucharczyk

EmberConf 2024 - Contextualizing State with Kevin Kucharczyk

Watch on YouTube

Video summary

Kevin Kucharczyk begins his presentation by addressing the challenge of maintaining consistent component APIs across both React and Ember frameworks within a unified design system. While he initially aimed to use React Context in his library, he realized this pattern was not natively available in Ember, forcing him to explore alternative state-sharing mechanisms like prop drilling, contextual components using `yield`, and global services. He explains that while these existing patterns are useful, they do not fully solve the problem of deep nesting where a grandchild component needs access to data provided at the top level without passing it through every intermediate layer. To bridge this gap between frameworks, he developed an Ember add-on called "Context" which introduces two new decorators: `provide` for injecting state into a specific component tree and `consume` for retrieving that state within nested components or template-only scenarios. The core of the talk demonstrates how these Context tools simplify complex UI patterns by eliminating verbose prop drilling and enabling more composable, headless libraries. Kucharczyk illustrates this with examples from form fields and accessibility attributes; instead of manually passing IDs and linking labels via `for` and `aria-describedby` attributes through multiple layers, a parent component can provide a context containing these values automatically. This allows child components to simply consume the necessary data without needing to know about internal implementation details or worrying about whether specific helper components are rendered. Furthermore, he shows how this approach facilitates functional patterns similar to React hooks, allowing developers to create reusable functions like `useSelect` that directly access state from a parent context, thereby reducing boilerplate code and improving developer experience when building complex form controls. However, Kucharczyk also highlights the importance of understanding Ember's reactivity model when implementing Context, as improper usage can lead to infinite rendering loops if tracked properties are not managed carefully within getters used for `provide`. He shares a debugging story where an accidental circular dependency caused his application to crash with a maximum call stack error. The solution involves either splitting large contexts into smaller, named ones or providing stable references like class instances rather than reactive getters that recompute on every change. Despite these nuances, he emphasizes that Context is simply another tool in the developer's toolbox and not an objective replacement for all other patterns; developers should still utilize contextual components when shipping tightly coupled parent-child pairs or rely on global services for app-wide concerns like authentication to choose the most appropriate pattern for their specific use case. In conclusion, Kucharczyk invites the community to adopt this new Context add-on in their projects and encourages them to share their own experiences with it via Discord. He notes that while an RFC exists to potentially integrate these features directly into Ember core, his immediate goal was to provide a practical solution for developers who want React-like composability without compromising on framework consistency. By offering helpers to inject contexts easily during testing without modifying templates, he aims to lower the barrier to entry and make state management in deep component trees more intuitive. Ultimately, the talk serves as both an introduction to how Context works in Ember today and a call for community collaboration to refine these patterns further before they might become part of the core framework in future updates.
Read the full video transcript
thank you for the introduction um sorry to disappoint you I do not have an Australian accent because I am originally from Poland or kind of from Germany the the accent is all over the place I currently live in Australia so this talk was initially supposed to be a little bit bit of a surprise sort of thing like oh what if Ember had contexts but the surprise has been spoiled by all the previous talks that have mentioned context already um so you know it already exists um but maybe not everyone knows what context is all about um some of you maybe have used react or spell or other Frameworks that already have context um so I'll introduce you to the cont concept first so don't worry we'll learn all together um but first so custom myio our main product is still an ember app but recently I found myself working with react projects as well and we're updating our company's design system and the component library that comes with it uh this is one of our testing screenshot with all the button States um due to the nature of our projects though we decided to build two component libraries at the same time so we have a separate library for react and Ember but they're both based on the same design system and the same tokens um but yeah whether that was a good idea at all um building two systems not sure if that was good uh but I'm not going to dive into that cuz that would be an entirely different talk like how to build two systems at the same time uh not today um but I found myself building these components in parallel for both Frameworks at the same time we also have a Rec cool story book set up that compares the components to each other again not today um but my aim was to keep the component apis as similar as possible because I wanted our developers to um still know what's happening and how to use the components when they switch between projects um and on the react side I used context to provide state to the components uh to the library components uh but then I realized that context is not available in Ember so I couldn't quite make the apis that similar because um obviously the internals would change you'd have to change the way how you invoke the components so I had to rethink things a little bit a little bit but before I dive into that uh let's talk about context itself first so by the nature of HTML the websites and apps that we build uh they're always these tree like structures HTML elements are nested in each other and they create branches that can be many layers deep and components in all the Frameworks that we use uh building on top of HTML they are also rendered in trees uh in a components template we can invoke simple HTML or we can invoke more components um building them to any sort of depth that we want and with components uh we can also pass down arguments deep down the tree the components also form relationships we can talk about parent components or child components referring to the components that are directly above or below uh or we can talk about ancestors and descendants talking about components that are um all the um components further up the tree or all the children and grandchildren of a particular component um and when we render these components we pass arguments into them which could be like State uh call backs options whatever we want and then as the complexity of complexity of our app grows the component tree can become quite deep uh we have components that render children and grandchildren and so on and sometimes the great grandchild component will need access to a variable that was passed in at the top level and the only way to get that variable down there is to pass it through each layer we have to explicitly pass that prop or argument through each component in that tree until it reaches the component that needs it and this repeating this passing of arguments is something we usually call prop Drilling and there's nothing really wrong with prop drilling as such it's something we have to do um but it can become tedious having to repeat arguments through all these levels sometimes uh and can also make our components a bit harder to maintain if we have to repeat arguments that sometimes are just not even relevant to one of the components it's just the component is somewhere along the way uh and it needs to pass this argument through it's it's just unnecessary and context is a way to solve that prop drilling problem in like a dependency injection sort of way um a context can be provided we say at a top level and then any component that renders as a child or grand child like somewhere in that tree deeper down within the provider can access that state like similar to Services where you just get this state almost out of nowhere um and if you work with react or view or spelt you probably used this already in react this is called create context and use context view has provide an inject and I now realized the links are a little bit cut off um so I'll share these slides later um so we have provided inject in view we have uh set context and get context in spelt and to put this into practice this is what it would look like in react you create a context object which itself doesn't do much it's just a representation of the state that you will be making available but it gives you a provider component which you can then render in your app and that provider component receives a value and that's the value that will be made available to all the children and grandchildren and then if you want to use that context retrieve the state in a component that's deeper down tree you call use context and give it that context representation that you created earlier and now you can pull out the data that you've provided somewhere further up the tree and anytime the data changes this component will also reender so it subscribes it to any sort of reactive State um so this is context it's quite simple it looks like service injections it's great um and if this is we'll have some em based examples along the way so don't worry so going back to my design system while the react components used context and they work quite well I couldn't build that same thing in Ember so I had to re-evaluate what I can do first before making any further decision so I stopped my development and I thought about what is possible to Ember right now and how that might apply to react because maybe I can just like Drop context and use amember patterns in react um the spoiler is that I didn't um but I did have to reevaluate what is available so I broke it down to three state sharing patterns essentially um we can pass data around with prop drilling which we've covered um Ember also has contextual components and services of course um which we'll dive into in a sec with prop drilling um if you were building component library or using a component Library you might agree that um if the library didn't have context didn't have context components it would be pretty annoying tedious having to pass around Arguments for the library like you can imagine a select or drop down menu component where the drop- down menu items uh need all the same arguments repeated that you've already passed into the select component it's super annoying super tedious so you don't want to write your component libraries this way which is where um other sharing uh State sharing patterns come in like contextual components you might already be thinking well I've been talking about prop drilling uh contextual components already solve that problem yeah they Doo if you're not familiar with contextual components um this is a quick reminder contextual components is when we uh yield use a yield keyword to um make certain components available as you consume another one so you combine the yield keyword with the component helper to uh provide the develop with components with partially curried arguments so that you don't have to manually pass those arguments in it's already like a preconfigured component basically so here the select components which will be an example that we'll use for a little bit in this talk could yield the child components like a label or an item with partially applied arguments already so the developer that's using this component doesn't have to repeat values that are internal and Tides into the component like a selected value you don't have to think about that when consuming a component which is great this is a wellestablished widely used pattern in Ember you're probably familiar with it and I'm a big fan of the composition patterns that this makes available like this is so unique and really cool about Ember uh this doesn't really exist in other compon other Frameworks this is one of my favorite features like the composition patterns just fantastic but it doesn't really uh fix prop drilling it doesn't remove prop dring it just moved the prop drolling elsewhere it it's still there if the yielded components the select items component um had more components deeper down the tree you'd still have to repeat all these props it doesn't actually solve the whole problem and then we also have services and services are probably the closest thing we have to context right now in Ember uh but they don't really ex um address the same problem services are Global you usually have you can have single um you can have multiple instances of services but you don't usually do that so a service is usually a Singleton instance Global throughout the app any component accessing a service would be accessing the same value so if you want to restrict the state that you're passing to a component tree um that's not going to do it this is global um it's not going to help solve your issues so before I continued building my components I did some thought experiments and you know where these thought experiments ended up cuz you already know that the add-on exists so this was supposed to be the sort of hypothetical what if scenario um but that's what I did so I started thinking about what if we did have context and I wrote pseudo code at this time when I was writing this down like this didn't exist I was just thinking what if so I thought that if there was context um like I said services are already quite similar to how I feel you might consumer context if we did have context I imagine we might have a decorator that I called consume you give the consume decorator a context name just like you give the service decorator a service name and it just Returns the value of um the context simple additionally U I feel like a context consumer component might be useful for usage in templates or template only components where you don't have a backing class to apply The Decorator to so there a context context consumer component where you give it the key which is again the context name and it yields the context value which you can then use in your template um I think this was shown in one of the previous slides the empata upgrade path so this is not entirely unfamiliar to you now but then how do we provide context I couldn't quite use the service patterns that we have available because again services are uh installed globally but context needs to be tied to the component trees it needs to live on the component level so I couldn't create a CL class that's um initialized and registered in the app so I started with The Decorator again because that's a pattern that we're familiar with we're comfortable with decorators uh and this one is called provide it's kind of similar to the um view provide function just naming you give it the context name which is the one that we use in the consume decorator and you attach it to anything you want really here it's attached to a getter and whatever the getter returns will be the value of to context you could also attach it to a stable reference or a a property or a class instance that you initialize it can be anything additionally uh there's a context provider component similar to the context consumer again to use your context in template only scenarios so from a developers perspective you don't need anything else to use context this is it this is uh two decorators two components uh you're ready to go so you know it took me a little bit of talking to just introduce two boring decorators to components uh that could be it you know I could say thank you get of stage but example is where context really starts to shine like we need to see it in action so uh let's have a look at some of those examples I am going to back to the select component that I mentioned before uh and I have to reiterate that contextual components work quite well for a select component you can see this in Ember Power select I feel like that's one of the most popular add-ons in the the community um and yeah there's nothing wrong with using contextual components for this but if we use context for this one I feel like the components API could simplify a little bit further or be a little bit more composable so the top one is the contextual version and the bottom one is the context version it's terrible naming bottom one is the context version um and in the new version the parents component would no longer have to yield any item components or Curry any arguments into them all it would have to do which is not pictured here uh that's an implementation detail it would only have to provide a context to be used for any components that are nested within um the code we would have to write as a developer to use the select component changes a little bit we no longer have to write as s to get access to whatever has been yielded um because the yield statements have been removed anything um that's R within component that that's it there there's nothing yielded we've also replaced the yielded label with a generic form field label component which we could then reuse across any sort of form field component U no matter where we use it because it uses context it would access the value with the consumed decorator no matter where it's put within the select component it would always have access to those values um so it could be universally used across many other components and then finally the select item component we no longer use any yielded once so we just invoke a regular component by its name so when you put these examples side by side it just highlights how similar these codes snippits are like what's the point um the developer that's using uh these new components maybe doesn't see a benefit immediately but the developer who builds the component the select component like a component Library developer or an M Power select developer can see a much larger benefit um say the select component lived in an add-on that um is like a component Library add-on uh many reusable components and you'd like to allow other developers to use these select components but also style them to match your your business and styling needs this uh library is called headless libraries referring to the fact that they don't have any Styles or like very minimal styles that are easy to override um and that's a common scenario so if we were trying to customize an addons contextual component that doesn't have any Styles or mineral Styles we may not have access to any of the internals to actually configure these things comfortably to apply our Styles we would have to wrap the select components and yield our own item components carrying all the same arguments that the original author already carried so we'd have to look at the original libraries code and basically duplicate the templates but with our own components um I've seen this a little bit in our app with Ember Power select where we have to look into the Ember Power select internals and copy over all the arguments so that we can customize the component it's possible but tedious so then for the select item component we'd have to do the same here we apply our own uh classes could be tailor classes whatever styling system we use uh but then again we'd have to copy over all these arguments that already are defined in some other Library it's it's a very annoying process and whenever anything changes in the other Library you have to copy off the arguments again it's a lot going on so if to add-on used context to provide the state a developer like me would have a much easy time trying to customize these components uh because the components are no longer as connected I can style my components in isolation knowing that the context that provides the value is accessible no matter where the components are so I can style my components in isolation I can rely on the fact that the select item component the one that actually does the work in ter will have access to the value no many no matter how many wrapper components I will need to add to achieve my styling needs and in fact you may not even have to use a select item component at all context would allow you to build things like things kind kind of like react hooks here uh you can imagine we could build a hook uh or function that's called use select which does basically the same thing that the select item component would do it gets access to the select select parent components context and gets all the values out of it and does certain computations that um you can then um use within your component to access all the state like you can build your select item component um making sure knowing that we have access to the context and here the use select function you can see that it doesn't have the uh provide or consume decorator attached like in the previous examples and this is a pattern that we could introduced that is similar to the use function from mro sources or the uh hypothetical service the functional form of service injection from Ember Polaris service uh so it's again a pattern that is already existing in Ember and we can do this because if we pass in this into our use select function the um use select function has access to the component instance and through that instance it has access to the owner the Ember app so we can hook in that function into whatever does the context tree internal tracking so if you've worked with react hooks uh this kind of looks like react Hooks and uh since we're talking about Forum elements let's look at some other Forum element examples uh we had a accessibility talk earlier today so accessibility is a good example to talk about um one common accessibility requirement is to provide labels for your phone controls and your phone controls could also have additional help text or error messages applied and to make these accessible uh you usually use additional attributes to connect all these together here the um label points to the input via the for attribute which points to the input's ID and then you have the description which is linked with the input via the area described by attribute which points to the paragraph's ID so if we used context we could build components that generate these attributes and then later set them automatically for us so when we're building our app we don't actually have to think about any of the accessibility details it all just hooks it up automatically for us so we're building a yeah I hope you can see this we're building a hypothetical generic form field component which we can then use as the parent component for any sort of form field like text input or select uh box and there's a few things going on here first we generate a unique ID for the whole group of elements if one isn't provided as an argument already we generate unique IDs for any elements that might exist within that component like the description or the input itself and we also create a modifier here which uh will then register the existence of a description to know whether a description has been rendered at all and you'll see in the next slide how this works and finally we take all these values and provide them in a context so that all the child and grandchild components can actually access the these values then we have the form field child components which might look like this I tried keeping it simple but it's very hard to fit all this code um so the individual form components they would consume the form field context and then set the necessary attributes and the description component it attaches the modifier that we made available in the form field which then lets the Forum field context know that the description has been rendered which the input then uses to render the area describe by attribute so then when we consume these components uh we don't have to worry about any of these internals we just render it like this and because the form label the text input everything is nested within form field all these uh context things are doing the setup for us this is already accessible you don't have to do anything extra you could pass in an ID if you'd like uh if you don't have to it's all done internally so as a developer I can compose these components in whatever order shape I want I can add any um wrappers which I might need for styling needs I can rearrange them and they always work the same if then nested within the form field so using context we could build libraries that make it even easier to do accessible stuff like we could have utilities and components that abstract this these things even further but then you run the code that I just wrote and it it it actually doesn't work um you get this maximum call stack size exceeded error um and yeah maybe you just shouldn't work with context at all I'm not going to ship this to production uh I don't think I can write good code at all and I think I saw a few skeptical faces in the audience a few slides back so let's go back to the form field and see what went wrong and this is just to highlight that anything you provide in context is just like regular reactive Ember code um so the problem that I'm seeing here is actually not unique to context at all this is just me not being careful about my ghs and my track properties so what happens here is uh the form field context is a getter which means that it will always recompute reender when any of the track prop references change uh you can see that the register description modifier which we've attached to the description component uh it sets the H description property uh which will have the form field context recompute which will have the description component reun the modifier which again will set the H description and it'll just keep running in this Loop that's why it breaks um and again this can happen easily um even without context if we're not careful about our gets um but for me it's it's particularly important with context to be very Vigilant about how we arrange our GS and our track properties because I feel like the the disconnection that exists between the components that provide and consume context just makes it easier to forget that these connections actually do exist so we have to be really careful about how we arrange our po properties but how do we fix this particular issue well one thing could be to break it up into multiple smaller contexts one component can provide multiple contexts as long as they all have different names um but of course we still run the risk of creating infinite renders here uh the actual problem here is fixed by removing the H description accessor from the description context that has been moved into the form control context uh and by the way as your app grows and you may have seen this in react context implementations if you have a lot of template based context your app is just going to keep growing sideways and there's nothing wrong with that like the app will still render just the same but if you're trying to debug this in EM insector you're going to have to scroll like very far to get to your actual apps content to try and like debug anything and with this implementation of context by using the provide decorator you can provide multiple context from one component so you might not see this issue at all which is great for Ember bad for react so back to the form now after we split up the big context into many smaller context the individual form components look like this the admittedly quite similar they just access their own um small individual contexts now um but in this refactor there's one additional benefit Beyond just fixing the bug previously I hope you noticed that the text input component was checking whether the description is rendered and then setting the area described by attribute which means that it has to know that a description component is a thing and that may render but now all it receives is the area described by attribute directly it doesn't have to worry about what components do or do not exist uh all it receives is what value do I set for what property it doesn't have to worry about any externals that are happening and this now renders this actually works great um there's alternative fixes um and I won't be diving into the details here but essentially what you have to do is uh remember that anything can recompute so if you wanted to fix these recompute issues you can also provide a stable reference to your context you can provide a class instance or you can provide a simple object that then includes smaller gets that recompute whenever a specific property changes and not when any property changes there's more use case for context Beyond just forms um you could provide form validation where your top level form component has a validation context which carries all the necessary uh State and functions to compute your validation error messages and hooks them into your um you know text inputs or select inputs automatically so you don't have to wor about passing through all that state um and there's many other use cases for this that I sadly don't have time to dive into today but I'd love to talk to you all about it later if you have the time um but of course context isn't always the solution to all our problems context it's just another tool in our toolbox to build really cool composable components it's not objectively better or worse than the other patterns it's just another thing we can use so you might still use contextual components if you want to uh ship your parent and child components together like if you have very uh particular usage patterns you can still ship those components together you don't have to use context if you know the uh if you want to deliberately restrict composition if your components only work a particular way you maybe you don't need context U just ship them as one thing um and also if you know that there's no deep nesting if there's a very straight parent child relationship you can ship them as contextual components you know there's not going to be any extra layers so just go for it uh and services of course will always be great for Global any sort of global uh information you can use it for authentication API handling logging whatever you can use context for that as well but typically you don't need to override things like um API handlers uh for particular component trees usually that'll still be fine as a global thing if you have a use case for um logging certain things for a particular component tree go for it context is great for that and finally how do you test with context so usually you would like to test your components in the same sort of environment that will run in in uh production so you want to run in under realistic scenarios which means that if you have something that provides the context that's what you want to use in your test if there's a component library that gives you the provider component you want to invoke that provider component in your test and wrap all your tests in that component so if you have a lot of integration tests for your component that consumes this provider you'd have to repeat this provider uh component in all your tests which might become tedious like if you have hundreds of these you always have to repeat the provider which you don't want to do so we could introduce a helper which I would love to see added in empit test helpers which uh maybe you should open up request for that but we could add a helper that allows you to override the test template for a group of tests um so you define that in your before each hook to override the test template for the whole module and then this uh will under the add-on provider outside of your component for all the tests in here so all the tests will use the provider already you can also use the context provider if you want more control over the particular values that you want to provide but if all you're trying to do is to overwrite the value for a particular context we can also introduce a helper that uh does just that without modifying any sort of template stuff we have a helper that's called provide which basically injects the context straight into the test so there's no extra template stuff happening uh you can this way give it the values that you want to use and I believe empid is already using this sort of thing in their tests or was trying to uh and this way you can also Pro provide multiple different contexts in your tests without having to do the whole nesting thing in your templates um and this was supposed to be a surprise um hey you can use context already that's an add-on that I wrote I didn't want to compromise in I uh my component Library I didn't want the apis to be different between react and Ember um and I didn't want to give up on context because I I really enjoyed the developer experience in react that context enabled the composability that was available so I went ahead and built an add-on that does it for Ember um which you can use these days and I've heard a couple of people are already be using that in their projects but of course there's also the context RFC which you may or may not have seen I believe context should be part of Ember and we're working on making that happen context based components already live in our production UI um that's been shipping for a couple months now and I'd like to invite all of you to try context in your add-ons or apps or test products and please talk to me about how you've used context in your apps tell me about your use cases um I'll be here now you can find me on Discord um and that's it thank you for listening and let's chat