I Ditched RJSF for This: My #1 JSON Display Fix (2025)
Tired of using the bulky RJSF for simple JSON display? Discover a lightweight, custom React component approach for 2025 that boosts performance and simplifies styling.
David Chen
Senior Frontend Engineer specializing in performant React applications and data visualization.
The Love-Hate Relationship with RJSF
For years, react-jsonschema-form (RJSF) has been the undisputed champion for generating web forms from JSON Schema. It’s powerful, feature-rich, and a lifesaver for projects with dynamic, configuration-driven UIs. I’ve used it, I’ve recommended it, and I’ve been grateful for its existence. But in 2025, my relationship with it has fundamentally changed. Why? Because I, like many developers, was using it for a job it was never truly designed for: displaying data, not just capturing it.
We often need to present complex JSON objects to users in a clean, human-readable, read-only format. The schema tells us the structure, the types, and the descriptions. So, the logical next step seems to be reaching for RJSF, disabling all the fields with `ui:disabled` or `readonly`, and calling it a day. This works, but it’s like using a bulldozer to plant a flower. It’s overkill, and it comes with hidden costs in performance, bundle size, and developer sanity. After wrestling with this approach on one project too many, I decided to find a better way. This is the story of how I ditched RJSF for display tasks and found my new #1 fix.
The Problem: Using a Form Generator for Display
The core issue is a misalignment of purpose. RJSF is built to handle form state, user input, validation, and submission logic. When you use it for read-only display, you're loading a massive engine just to use its steering wheel. Here are the specific pain points that finally pushed me over the edge.
Pain Point #1: Performance and Bundle Size
RJSF is not a lightweight library. It includes logic for every possible input type, validation keyword, and UI configuration. When you only need to show a string, number, or nested object, you're still shipping the entire form-generation engine to the client. This can add a significant, unnecessary weight to your application's bundle size, leading to slower initial load times. For complex, deeply nested objects, the rendering performance itself can become sluggish as RJSF churns through its complex component tree designed for interactivity.
Pain Point #2: The Customization Rabbit Hole
Want to change how a specific field looks? In RJSF, this means diving into the world of custom widgets, fields, and templates. You might find yourself writing a whole new React component just to render a string with a different style or layout. While powerful for forms, this level of ceremony is incredibly cumbersome for simple display purposes. You end up writing more boilerplate to undo or override RJSF's default form-like behavior than you do on the display logic itself. It feels like you're constantly fighting the library's primary purpose.
Pain Point #3: Styling Conflicts
RJSF generates markup that is semantically a form. You get `<form>` tags, `<fieldset>`s, `<label>`s, and `<input>`-like structures. Trying to style this to look like a clean, elegant data display often involves messy CSS overrides. You battle specificity wars against the library's default theme (e.g., Bootstrap) and write fragile selectors to target the elements you want to change. This is brittle and makes future maintenance a headache.
The Epiphany: The Right Tool for the Job
My "aha!" moment came during a code review. I was looking at a massive `uiSchema` object filled with `"ui:disabled": true` and custom CSS classes just to make an RJSF-powered view look like a simple definition list. It was complex, hard to read, and inefficient. The realization was simple but profound: we don't need a form generator to display data from a schema. We need a schema-aware data renderer.
The fix isn't another heavyweight library. The fix is a shift in mindset. Instead of adapting a form tool, why not build a tiny, dedicated component that does exactly what we need? It turns out, this is surprisingly easy and unlocks a world of flexibility and performance.
The Solution: A Lightweight, Recursive React Renderer
My #1 fix is a custom, schema-aware recursive React component. This sounds more intimidating than it is. The core idea is a single component that knows how to render a piece of data based on its corresponding schema type (`string`, `object`, `array`, etc.). When it encounters an object or an array, it simply calls itself for each item, passing the relevant sub-schema and sub-data. This approach is incredibly powerful.
The Core Rendering Component
At its heart, the solution is a component, let's call it `SchemaDataViewer`, that takes two props: `schema` and `data`. Its primary job is to look at `schema.type` and decide which sub-component to render.
// SchemaDataViewer.jsx
import React from 'react';
const SchemaDataViewer = ({ schema, data }) => {
if (data === undefined) return null;
switch (schema.type) {
case 'object':
return <ObjectRenderer schema={schema} data={data} />;
case 'array':
return <ArrayRenderer schema={schema} data={data} />;
case 'string':
return <StringRenderer schema={schema} data={data} />;
case 'number':
case 'integer':
return <NumberRenderer schema={schema} data={data} />;
case 'boolean':
return <BooleanRenderer schema={schema} data={data} />;
default:
// Render data as a string if type is unknown
return <span>{String(data)}</span>;
}
};
export default SchemaDataViewer;
Handling Data Types and Nesting
The magic happens in the `ObjectRenderer` and `ArrayRenderer` components, where recursion comes into play. The `ObjectRenderer` iterates through the keys defined in `schema.properties` and, for each key, renders another `SchemaDataViewer` instance.
// ObjectRenderer.jsx
const ObjectRenderer = ({ schema, data }) => {
const properties = schema.properties || {};
return (
<div className="object-container">
{Object.keys(properties).map(key => (
<div key={key} className="property-pair">
<strong className="property-title">{properties[key].title || key}:</strong>
<div className="property-value">
<SchemaDataViewer
schema={properties[key]}
data={data[key]}
/>
</div>
</div>
))}
</div>
);
};
This simple recursive pattern elegantly handles any level of nesting in your JSON data, all while using the schema to provide context like titles. The entire renderer can be built in just a few dozen lines of code.
Styling Freedom
Because you control the markup completely, styling becomes a dream. You're not fighting against pre-existing framework styles. You can use simple CSS, CSS Modules, or your favorite CSS-in-JS library like Styled Components or Emotion to create a beautiful, bespoke display that perfectly matches your application's design system. The markup is clean, semantic, and exactly what you want it to be.
RJSF vs. Custom Renderer: A Head-to-Head Comparison
To make the difference crystal clear, here’s a direct comparison for the specific use case of displaying data.
Feature | RJSF (Used for Display) | Custom Recursive Renderer |
---|---|---|
Primary Use Case | Interactive form generation | Read-only data display |
Bundle Size | Medium to Large (includes all form logic) | Tiny (only what you build) |
Performance | Can be slow with large/nested schemas | Extremely fast, minimal re-renders |
Customization | Complex (requires custom widgets/templates) | Simple (just edit the React component) |
Styling | Often requires CSS overrides and fighting defaults | Full control with clean, semantic markup |
Developer Experience | Cumbersome; fighting the library's intent | Intuitive; direct and explicit control |
Time to Implement | Fast for defaults, slow for customization | Slightly more setup, but faster for custom looks |
When Should You Still Use RJSF?
Let's be clear: this isn't a declaration of war on RJSF. It remains an excellent tool for its intended purpose. You should absolutely still reach for RJSF when:
- You need to generate interactive forms for creating or editing data.
- You rely on its powerful, built-in JSON Schema validation to provide real-time feedback to users.
- You need to handle complex form logic like conditional fields (`dependencies`) or `oneOf`/`anyOf`.
- You need to quickly scaffold a standard-looking form and are happy with its default theming.
In these scenarios, RJSF is a massive time-saver and the right tool for the job. My argument is simply to stop using it where it doesn't fit.
Conclusion: A New Default for JSON Display
Ditching RJSF for read-only display tasks has been a game-changer for my projects. By embracing a simple, custom recursive rendering component, I've gained massive improvements in performance, bundle size, and developer happiness. The code is simpler, the styling is easier, and the end result is a UI that is purpose-built for displaying data, not capturing it.
So, the next time you need to render a JSON object based on its schema, I urge you to pause before you type `npm install @rjsf/core`. Ask yourself if you truly need a form generator. If the answer is no, take ten minutes to build a small, recursive renderer. It’s a simple fix that will pay dividends in the long run, and for me, it has become the new default for JSON display in 2025.