Tag: Component

  • When a custom LWC beats a standard component, and when it doesn’t

    Every org I’ve worked on has at least one custom Lightning Web Component that didn’t need to exist. Usually it’s a record highlights panel rebuilt pixel-for-pixel to match a mockup, or a related list reimplemented because someone didn’t like the default row height. Three years later, nobody remembers why it’s custom, and it’s the thing that breaks every release.

    The opposite mistake happens too. I’ve seen teams force a genuinely complex screen flow into standard Flow screen components, bolting on formula fields and hidden sections until the logic is unreadable, because “we try to avoid custom code.” Both failures come from the same place: treating the custom-vs-standard call as a style preference instead of an engineering decision.

    Start from the default, not the exception

    Standard components should win by default. Lightning App Builder components, Dynamic Forms, Flow screen components and the standard related lists get platform upgrades for free, inherit accessibility fixes automatically, and don’t need a developer on hand every time field-level security changes. A junior admin can maintain a page built from standard components. A custom LWC usually needs whoever wrote it, or a very good comment trail, because the logic lives in JavaScript, not in clicks.

    So the real question isn’t “can I build this as a custom component.” Almost anything can be built as a custom component. The question is what you’re giving up to do it, and whether what you get back is worth that cost. Most of the time it isn’t.

    Where standard components actually fall over

    Cross-record logic in a single view

    Standard related lists and highlights panels are bound to one record and its direct relationships. The moment you need to pull in data from a grandparent object, aggregate across a child relationship, or show a calculated value that depends on more than a formula field can express, you’ve hit the ceiling. I built a component a while back that showed a rolling 90-day case volume trend against SLA breach count, on the account page, computed from two child objects with different filter criteria. No combination of standard list views and report charts got close, and a custom LWC calling Apex was the only honest answer.

    Conditional UI that standard components can’t express

    Dynamic Forms handles field-level conditional visibility well. It does not handle “show this entire section, with this specific layout, only when three unrelated conditions are all true and the user has one of two permission sets.” You can hack it with formula fields and record types, and I’ve watched teams spend more hours fighting that hack than the custom component would have taken to build properly.

    Inline actions that need immediate feedback

    Standard quick actions post back and refresh. If a contact-centre agent needs to trigger an action and see the result in under a second, without a page refresh breaking their flow, a declarative quick action often feels sluggish enough that agents route around it. That’s a real usability cost, not a cosmetic one, and it’s the kind of thing that shows up in average handle time six months later.

    What building custom actually costs you

    This is the part that gets skipped in the planning meeting. A custom LWC means:

    • Someone owns the Jest test suite, and it has to stay green through every org upgrade
    • Accessibility is now your responsibility, not the platform’s, down to keyboard navigation and screen reader labels
    • Every new field or business rule change is a deployment, not a configuration change an admin can make on a Friday afternoon
    • The component needs its own security review, because it’s making its own SOQL and Apex calls instead of relying on standard field-level security enforcement

    None of that is a reason to avoid custom components. It’s a reason to make sure the thing you’re building actually needs to be custom, because that list of costs doesn’t go away just because the component works on day one.

    A decision framework that holds up under pressure

    When a requirement lands on my desk that looks like it needs a custom component, I run it through four questions before anyone opens VS Code.

    Can Dynamic Forms, a Flow screen component, or a standard related list get within 80% of this with configuration alone? If yes, build that first and show the stakeholder. Half the time the remaining 20% turns out to be a nice-to-have, not a requirement.

    Does the logic depend on data the platform can’t surface declaratively? Cross-object aggregation, external callouts rendered inline, or calculations too complex for a formula field are the honest triggers for custom code. A preference for a different button colour is not.

    Will this component need to change often, and who will change it? If the answer is “the business team, frequently,” lean hard toward standard, even if it means a slightly less polished result today. If it’s “rarely, and only a developer touches it,” custom is a safer bet.

    What’s the blast radius if this breaks during a Salesforce release? A standard component degrades gracefully, usually. A custom LWC with an unhandled API version bump can take down a page entirely. If the component sits on a page every agent uses every call, that risk needs to be priced into the decision, not discovered after a Sunday release.

    What I actually check in code review

    When a pull request adds a new custom LWC, I’m not reviewing whether the code is clean, not first. I’m checking whether the PR description explains why a standard component couldn’t do the job. If that explanation isn’t there, the review stops until it is. That one habit has quietly reduced the amount of unnecessary custom UI across every team I’ve led, more than any architecture guideline ever has.

    The components worth building custom are the ones that would be genuinely painful to build any other way. Everything else is a related list with better padding, and the platform already gives you that for free.

  • Designing a Salesforce solution around one metric leadership will actually track

    Ask most delivery teams what a Salesforce enhancement is meant to achieve and you’ll get a feature description back. “We’re adding a custom object for renewals.” “We’re building an approval process for discounts over 15%.” Ask what number on a leadership dashboard is supposed to move because of it, and you’ll usually get a pause.

    That pause is the problem. A solution built without a target metric gets evaluated on whether it shipped, not whether it worked. Six months later nobody can say if the renewals object actually reduced churn, or if the approval process actually cut discount leakage, because nobody wired the build to a number in the first place.

    Why “it works” isn’t the same as “it moved something”

    I’ve sat in enough steering committees to know the difference between a project that’s technically complete and one that’s actually earning its budget. The first gets a green status and a demo. The second gets renewed funding next year because someone can point at a chart and say “that’s us.”

    Most orgs I’ve reviewed have dozens of automations, reports and custom fields that were built for a stakeholder request, went live, and then quietly stopped being anyone’s responsibility. Nobody’s watching whether they’re doing anything. The gap isn’t in testing. It sits earlier, at the requirements stage, long before a single flow gets built.

    Pick the metric before you open the object manager

    Before any real design work starts, force one question: what single number does this need to move, and who already looks at that number every week? Not a KPI you’ll invent for the project. One that already exists on someone’s dashboard, in someone’s board pack, or in someone’s head when they walk into a Monday meeting.

    For a contact-centre client I worked with, that number was average handle time on a specific case type. Not “improve agent efficiency.” Not “modernise the case workflow.” Handle time, on that case type, measured weekly, already tracked by the operations lead before we touched anything. Every design decision after that got tested against one question: does this reduce handle time on that case type, or doesn’t it?

    That constraint kills a lot of scope creep on its own. A nice-to-have field that doesn’t touch the metric gets deprioritised without an argument. A workaround that adds three clicks but saves the agent from a phone call gets fast-tracked, because someone can show the maths.

    What changes in the technical design

    Once you know the metric, it should show up in three places in the build, not just the final report.

    • The data model needs a field or relationship that captures the event driving the metric, at the point it happens, not reconstructed later from case comments or a separate spreadsheet.
    • The automation needs to fire close enough to that event that the data is accurate. A nightly batch job that recalculates handle time after the fact is a different design than a trigger or flow that stamps timestamps as the case moves through statuses.
    • The reporting layer needs to expose the metric in a way the person who owns it can actually check without asking IT for a custom report every Friday.

    Skip any of those three and you end up with a metric that technically exists but nobody trusts, because getting to it takes fifteen minutes and a pivot table.

    A composite example: the discount approval that didn’t cut leakage

    A typical enterprise org I’ve seen runs something like this. Finance flags discount leakage as a problem. The response is an approval process: discounts above a threshold route to a manager before the quote goes out. It ships. Everyone moves on.

    Eighteen months later, leakage hasn’t moved. The approval process captured whether a discount was approved, not what happened to margin as a result. Nobody built a field for the original list price versus the approved price at the line-item level. Nobody reported on override frequency by manager. The process existed, the metric didn’t, and finance is back asking for the same fix under a different name.

    More approval steps won’t fix that. Going back to the one number that actually matters, margin retained per deal, and checking whether the data model can produce it today, will. Usually it can’t, because the build was scoped around the workflow rather than the measurement.

    The scoping conversation this forces

    When a stakeholder brings a request, I ask them to finish this sentence: “I’ll know this worked when [metric] moves by [amount] by [date].” If they can’t finish it, the request isn’t ready for a solution design session yet. It’s a blunt filter, and a genuinely useful one for separating a real business problem from a feature someone saw in a demo.

    It also changes who signs off. A feature list gets approved by whoever asked for it. A metric commitment gets approved by whoever owns that number, and that person has a much stronger incentive to push back on scope that won’t move it.

    What this looks like in a solution design document

    Practically, add one section before the requirements table: the target metric, its current baseline, who owns it, and how the proposed build is expected to shift it. Keep it to a few lines. If you can’t fill it in, that’s the signal to go back and ask better questions before committing engineering time.

    I’ve started refusing to take a build past discovery without this filled in. It’s cut the number of “why did we build this again” conversations down to almost none, mostly because writing the metric down forces a conversation that usually gets skipped entirely.

    Where this earns its keep

    The payoff isn’t just cleaner requirements. It’s a much easier renewal conversation for whoever’s paying for the work. “We shipped the feature” competes for budget every year. “We moved handle time by 22% on the highest-volume case type” doesn’t need to compete. It gets funded again without a fight.

    That’s the actual argument for doing this on every enhancement, not just the big ones. Small requests accumulate into an org nobody can explain the value of. One metric, tracked from design through to the dashboard, is what turns a backlog of tickets into a track record.

  • Lightning Web Components: building for reuse, not just for one screen

    Most Lightning Web Components I inherit were built for one screen and never touched again until the second screen needed almost the same thing. Someone copies the folder, renames it, tweaks a field reference, and ships it. Six months later there are four components doing the same job with four slightly different bugs. Nobody planned that. It’s just what happens when reuse isn’t a design decision from the start.

    Building for reuse doesn’t mean over-engineering a component before you know if it’ll ever be reused. It means making a small number of choices early that keep the door open, so the second and third use case are a configuration change instead of a fork.

    What reuse actually means here

    Reuse in LWC isn’t about component count. A component that only ever lives on one page can still be badly designed, and a component copied into five places can still be tightly coupled if every copy needed manual edits to work. The real test is simpler: can someone drop this component onto a different object, a different record type, or a different flow without opening the JavaScript file?

    If the answer is no, it’s not reusable. It’s duplicable. Those are different things, and the difference shows up in maintenance cost about a year later, usually right when nobody remembers why there are four almost-identical versions floating around.

    The habits that quietly block reuse

    Hardcoded object and field references

    A component that queries Case fields directly in its JavaScript, or references a specific record type by hardcoded Id, works exactly once. The next team that wants the same UI on Work_Order__c can’t reuse it. They fork it. Now there are two components with the same template and diverging logic, and the next bug fix has to land twice.

    The fix isn’t complicated. Pass the object API name, field list, or record type as a public property (@api) instead of writing it into the component. Wire adapters like getRecord and getFieldValue already support dynamic field references built from those properties. Most teams know this. Fewer actually do it under a deadline, which is exactly when the shortcut gets taken and never revisited.

    Business logic sitting where the template can see it

    When validation rules, formatting logic, and layout are all written in the same JavaScript controller, changing one thing risks breaking another. A component that decides both “how do I calculate this discount” and “how do I render this card” is doing two jobs badly instead of one job well. Split the calculation into its own module, imported where needed, and let the component’s controller focus on wiring data to the template. That calculation module can then get reused in a completely different component, or in Apex-adjacent client logic, without dragging the UI along with it.

    One component trying to handle every variant

    The opposite failure is just as common: a single component with a dozen boolean @api flags controlling which sections render, because someone kept bolting new requirements onto the original instead of composing smaller pieces. I’ve seen components with eleven optional properties where changing one variant meant testing all eleven combinations to make sure nothing else broke. That’s not reuse. That’s a maintenance trap wearing a reuse costume.

    Patterns that actually hold up

    Public properties over internal fetches

    Where possible, have the parent pass data down rather than having every child component independently wire its own Apex call or record query for the same underlying data. It’s tempting to make each component self-sufficient, but five components each running their own query for the same account fields multiplies API calls and cache misses for no real benefit. A parent that fetches once and distributes via properties is both faster and easier to reason about.

    Events up, properties down

    Keep the data flow direction consistent. Parent to child is properties. Child to parent is custom events. A component that reaches up into its parent’s state, or that manipulates a shared record outside its own boundary, breaks the moment it gets moved into a different parent context. If a component only ever communicates through properties in and events out, it can be dropped into an entirely different page without anyone reading its source code first.

    Composition over configuration flags

    Instead of one component with many optional sections, build a few small, focused components and compose them in the parent template. A “record header,” a “related list,” and an “action bar” as three separate components are each easier to test, easier to reuse individually, and easier to reason about than one twelve-property mega-component trying to be all three depending on which flags are set.

    A working example

    On a contact-centre implementation I worked on, the original requirement was a compact case summary card for the agent console. Six weeks later, the same card was wanted on the account page, then on a custom Utility Bar panel, then embedded in a Flow screen. Because the first version had already pulled its object, its field set, and its “compact vs full” display mode out into public properties, none of those three later requests needed new component code. They needed configuration in the Lightning App Builder and, in the Flow case, a handful of input variables mapped to existing properties. What would have been three separate build tickets became one afternoon of admin configuration.

    That’s the actual payoff of designing for reuse. It’s not a cleaner GitHub repo. It’s fewer tickets landing on a developer’s desk for something an admin could configure themselves, if the component had been built to allow it.

    When it’s genuinely not worth it

    Not every component needs this treatment. A one-off internal tool used by two people, or a component built for a feature that’s explicitly temporary, doesn’t need public properties and composed sub-components designed for a future that may never arrive. Over-abstracting a throwaway component wastes time that should go toward the components that actually sit on multiple pages: record headers, related list variants, quick-action panels, anything that shows up in more than one Lightning page across the org.

    The judgment call is knowing which is which before you start typing, not after the third fork shows up in a pull request.

    A quick check before you build

    • Does this component reference a specific object, field, or record type directly in the JavaScript? If yes, can it be a public property instead?
    • Is there business logic mixed into the same file as the rendering logic? If yes, can it move to its own module?
    • Does the component need to reach outside its own boundary to work, touching parent state or global variables? If yes, that’s a sign it won’t survive being moved.
    • Would a second use case require someone to open and edit the JavaScript, or just change configuration? The second answer is the one you want.

    None of this takes much longer to build the first time. It just means resisting the urge to hardcode the one thing you know for certain right now, because the certainty rarely lasts past the next sprint.