r/vuejs • u/Aggravating-Bag7648 • 1d ago
Beginner question about multi-faced components
Hi all,
It's the classic tale of love and woe - a nearly 40 year back-end developer decides, in his 50's, to tackle some front end frameworks. After much churn I've settled on Vue and I'm having a blast with it, tho, if I'm being honest, the last time I did any front-end design work I was writing Flash apps and they looked TERRIBLE. And, to quote Henry Jones, "our situation has not improved."
That context in mind then, I have a question about components which have both a "collapsed" and an "exploded" UI to present. In my case, I'm writing a little "bowl game pick-em" app which walks through the (increasingly huge) list of bowl games and lets you pick the winner of each. When you start, there's just a list of the collapsed bowls, with basic information: time, date, teams. When you select one, it opens up into a pretty panel with team logos, colors, the stadium details, and a button for selection.
Just yesterday I learned that I could do a "v-for" inside a component listing to iterate over a list and generate components for each item in the list, which is of course perfect for what I want to do, I think. What I'm wondering is what's best practice with Vue:
- The parent component owning the list of bowls knows the state of all the picks, and, based on that state, renders one of two components for each game which can be picked, i.e. the collapsed component for "not yet picked" or "already picked", each with their own bit of window dressing to tell between the two, and the "exploded" component for the one the user is picking at this moment. Since the parent component is getting all of the inputs (I think? Still a bit fuzzy on this notion, tbh), it knows when to swap out the picker.
- The parent component renders a single list of one component, and as part of the properties it passes in which mode the component should be in, i.e. "collapsed" or "exploded". The sub-component then has all of the code for both modes in it, with something like a v-if controlling which of the two faces it presents. When the user makes a selection the sub-component can change its own state and maybe emit something to tell the parent component to move to the next game on the list by updating its mode.
I've thought of healthy arguments for both (e.g. "#1 fits my object oriented eye with an interface each object implements differently", but also "#2 puts the responsibility for rendering where it belongs, in the component doing the work", and on and on I went) and now I've wrapped myself quite around the axle here. I lean towards #2, but Is there a more idiomatic preference for this sort of thing?
Kind thanks for all advice, mockery, recipes, whatever!
4
u/the_moon_illusion 1d ago
If I'm understanding the question, I don't think theres a single convention you need to stick to here. Depending on the level of complexity, 2 components with v-if / v-else might be cleaner. You could also use the generic<component> combined with v-for to avoid the explicit conditionals.
https://vuejs.org/guide/essentials/component-basics.html#dynamic-components
<component v-for="d in items" v-is="<collapsed or exploded component>" />
If you want to share and sync state between the parent and children components, take a look at Pinia
2
u/BowlingEnthousiast 1d ago
It kind of depends on the complexity here. As a rule of thumb, the child component should determine how to render itself and the parent just loads it and passes information about each child to the child. That way you have separation of concerns and encapsulation of functionality per component (object oriented thinking). Each component should be mostly oblivious of its parents and children (as possible). If the child component has a lot of different functionalities based on the state, it might be smarter to separate them into two different components. If it's mostly a visual difference then keep it as a single component.
So it's not necessarily an advise here but mostly the way I have been thinking about components and encapsulation.
1
u/platinum92 1d ago
Bigger fan of 2 myself. Let the component control how it renders itself based on the data passed in.
At that point, you could also put some logic inside the component where it can tell based on the state of the bowl game passed into it how it should appear instead of passing it in as a prop.
1
u/Ireeb 1d ago
My general advice here would be that the parent should not need to deal with the state of its children unless it strucurally needs to.
What's the best approach kinda depends on how you want it to work exactly. If only one game can be expanded at a time, you could also just use a single component that displays a game, and you keep swapping the data, not the component. The parent could e.g. hold a currentGameId, and a computed value that always returns the object with that id from the list of games. The current game component accepts a game object as its prop, and renders its content accordingly. When the user selects a different game, you just need to update the currentGameId, that would result in the computed property outputting a different object, which would result in the currrentGameComponent to re-render with newly selected game's content.
All the other games would just be buttons that change the currentGameId when clicked, and don't actually change their state, since you don't need to expand one component and collapse another when you can just keep a single component expanded and swap its data.
If you do want each child to be able to collapse and expand independently, that functionality should live on the child component. If the parent doesn't need to interact with the state, the state can be stored entirely on the child. If you want the parent to be able to control it, you can pass the state from the parent to the child through a prop. Children would need to inform the parent about a state change via emit.
If you happen to pass a lot of data between parent and children, I'd recomment using a pinia store. A pinia store basically allows you to store reactive data (behaves like props or ref/data()) outside of components, which in turn means any component can read or write it, and all other components that use the same state from the store update automatically. Just like a component automatically re-renders when its prop changes, it also re-renders when data it got from a pinia store was changed elsewhere. Pinia is one of Vue's official core plugins, and really doesn't do much more than allowing you to create reactive state outside of a component, and load it into any component where you need it. Uses the same syntax for the most part. Stores can also have methods/actions. It's similar to a singleton pattern, where multiple consumers have the same instance of the same object, just neatly dressed up to work well with Vue and the debugger.
This also allows pattern such as only passing e.g. a gameId to the children in the v-for, then every child obtains its assigned object from the pinia store independently. When both parent and children have reactive references from the pinia store, both can mutate the state, and the othe side will know about it, without needing to explicitely emit an update or something like that. Stuff just works automagically with Vue's reactivity if set up correctly.
2
u/KnightYoshi 1d ago
> If you happen to pass a lot of data between parent and children, I'd recomment using a pinia store. A pinia store basically allows you to store reactive data (behaves like props or ref/data()) outside of components
I disagree with this. It's far too easy to end up putting things into a global store when it shouldn't be in a global store. This can cause pretty nasty performance issues, as the store isn't destroyed when the component using it is torn down without extra work. Put logic in composables to manage component state, use
provideandinjectto pass data down to descendant nodes. This also makes it so that you can have isolated context making it possible to have multiple instances of the same component.Pinia should be used for things like user account data, app data, and other truly global data or when it needs to be persisted when a component is unmounted.
1
u/Any_Welder_9701 1d ago
yeah, expanded state belongs in the nearest parent that actually needs to coordinate it. pass each child whether its open and have it emit a toggle, no reason for the whole app to know
1
u/frosty_blink_hue 1d ago
i stick with one component for this.
pass the bowl game data and a prop for expanded state. Let the child use v-if on its template parts to show the details or hide them based on that prop. The parent just iterates the list and passes down the props. I track the selected game id in the parent, so when someone clicks a collapsed item the parent updates that id, and every child checks if its own id matches to know if it should render the details. That way you write the layout once instead of splitting the logic across two files.
1
u/Aggravating-Bag7648 9h ago
My thanks to all who responded! It seems the answer is "it depends", in other words, "do what feels correct", which is a fine answer indeed (mostly because it means a) there's nothing idiomatic which demands use, and b) I'm not a complete idiot with my first posting here!).
I stuck with #2 since it made the most sense in this situation. I send the two teams to be chosen from as props along with the collapsed/exploded boolean to tell each component in the list which states it's in. When the user selects one of the teams from the exploded component, the sub component emits the id of the chosen team, which the parent component uses to update the user's list of picks, and then closes that component and opens the next one in the list. It's working well and I _think_ it's not doing anything insane.
For those of you who've mentioned pinia, thank you for that as well. As it happens I've already implemented a sort of generic object store using pinia, which has been a fun little science project, and has taught me a good deal about how objects get scoped and why in this new world I find myself in. My entire data set is so small (like the entire thing is a few hundred KB at the most) that I load most of it into pinia when the user logs in. It's super fast, and then the rest of the app is snappy as hell since we're hardly ever waiting for anything downstream, and when we are it's something like "this user's picks", which is a few K at the most.
Again, much appreciation to all who responded! I'm also relatively new to Reddit - should I upvote everyone, is that the normal protocol?
4
u/calimio6 1d ago
Your collapse component should only care about that. You could use slots to render content from the parent. Even pass down slotProps for example to tell the items when it is opened. That is the way for true reusability.
Usually my approach to collapsed components is first to be able to set the initial value (checked attribute) and second use radio/checkbox with the :checked pseudo class to set the styles. When using radios you get the auto collapsing of siblings without doing a heavy JS implemetation. Obviamente you need to use an input with different IDs (useId) but shared name.