If you've been building in Retool for a while, you already know more React than you might expect. When you bind a Table to {{ getUsers.data }} and watch it refresh after the query reruns, you're relying on the same ideas React is built on: components, state, and re-rendering when data changes. The difference is that Retool handles the wiring for you.
That's changing. Retool's new app builder generates apps written in React 19 on the frontend and TypeScript on the backend, and its Code tab lets you view and edit every file in your app. In our look at the Retool Classic App Converter, one of the cases where we suggest holding off on migrating is a team that isn't yet comfortable with React. The builder's AI can take care of most routine edits, but when something breaks, someone on the team needs to be able to read and fix the React code, even if they use AI to help. You don't want a situation where a business-critical app breaks, the AI's fix doesn't hold, and nobody on the team can read the code to find out why. If that skill isn't there yet, it's worth building it before you move the apps your business relies on...
The good news is that the jump is smaller than it looks. The hard part isn't the syntax - it's that React makes you do explicitly what Retool does implicitly. In this guide, we'll map the Retool concepts you already use to their React equivalents, so the code behind your apps reads as something familiar rather than a black box.
What we'll cover
- State: from temporary state variables and component values to useState
- Hooks: how useEffect replaces "Run on page load" and automatic query reruns, plus a brief look at useMemo and useCallback
- Data fetching and caching: loading states, errors, and what happens to query caching
- State at scale: context, and when a dedicated state library is worth it
- Front-end differences: routing, styling, components, and debugging
This isn't a full React course. We're assuming you're comfortable with JavaScript from writing transformers and JS queries in Retool. If you want to go further, the official React docs are the best place to learn the fundamentals, and our look at the Retool Classic App Converter covers what to expect when moving existing apps to the new builder.
State: from temporary state to useState
In Retool, you create a temporary state variable, set it with variable1.setValue(), and read it anywhere with {{ variable1.value }}. React's equivalent is useState:
const [selectedUser, setSelectedUser] = useState(null);
This gives you two things: the current value (selectedUser) and a function to change it (setSelectedUser). Call the setter, and React re-renders the component with the new value - the same way a Retool component updates when a variable it references changes.
There are two differences worth knowing early.
State is local by default. In Retool, any component or query can read any variable. In React, state lives inside the component that created it. If another component needs it, you pass it down as a prop. This is the biggest mindset shift, and we'll come back to it in the State at scale section.
The new value arrives on the next render. After calling setSelectedUser(user), reading selectedUser on the next line still gives you the old value. React applies the update when it re-renders. If you've been caught out by setValue() being asynchronous in Retool, this will feel familiar.
What about component values? In Retool, every input exposes its value automatically - textInput1.value is always there. In React, you usually hold that value in state yourself and pass it to the input. This is called a controlled input:
const [search, setSearch] = useState("");
<input value={search} onChange={(e)=>setSearch(e.target.value)}/>
It's more code, but it means you always know where a value lives and who can change it.
Hooks: useEffect and friends
Hooks are functions that let a component "hook into" React features like state and side effects. useState is one. The next one you'll see everywhere is useEffect.
useEffect: running code when things change
In Retool, a query can run on page load, or rerun automatically when one of its inputs changes.
useEffect covers both:
useEffect(() => {
fetchUsers(statusFilter).then(setUsers);
}, [statusFilter]);
The array at the end is the dependency array. It tells React when to run the effect again:
- [statusFilter] - run on first render, then again whenever statusFilter changes. This is Retool's "rerun when inputs change."
- [] - run once, after the first render. This is "Run on page load."
In Retool, dependencies are tracked for you. In React, you list them yourself. Leave one out and your effect uses stale data; add one that changes on every render and it runs in a loop. Most editors will warn you about missing dependencies - it's worth taking those warnings seriously. Say you forget to list statusFilter: the effect only runs once, so when a user changes the filter, the table keeps showing the old results - with no error to tell you why. Bugs like this are easy to miss in testing, because the app still looks like it's working.
useMemo and useCallback, briefly
useMemo is the closest thing React has to a Retool transformer. It calculates a value from other values, and only recalculates when those change:
const activeUsers = useMemo(() =>
users.filter((u) => u.status === "active"),
[users]
);
useCallback does the same for functions. It keeps a function from being recreated on every render, which matters when you pass it to a child component or list it as an effect dependency. React treats a recreated function as a brand-new value. So if an effect depends on it, the effect runs again on every render. For example, refetching data each time anything on the page changes… and a child component that receives it re-renders even when nothing it displays has changed.
Both are performance tools, not requirements. Start without them, and reach for them when a calculation is slow or you're seeing unnecessary re-renders.
Data fetching and caching
In Retool, a query gives you a lot for free: query.data, query.isFetching, query.error, success and failure event handlers, and optional caching. In plain React, you build each of those yourself:
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUsers()
.then(setUsersHandler) // success handler
.catch(setErrorHandler) // failure handler
.finally(() => setLoading(false));
}, []);
That's three pieces of state and an effect for one query. It works, but it gets repetitive quickly, and it still doesn't cover caching, retries, or refetching.
Caching doesn't exist out of the box. In Retool, you can tick "Cache the results of this query" and clear it with invalidateCache(). React has no built-in equivalent - useMemo avoids recalculating within a component, but it won't stop a request from firing again. For that, most React teams use data-fetching libraries, which give you loading and error states, caching, and invalidation in a single hook. If you've used Retool queries, their APIs will feel familiar.
In the new Retool app builder, data access runs through backend functions that the builder generates for you, so you won't write every fetch by hand. But knowing the pattern underneath makes it much easier to follow the generated code and debug it when a request behaves unexpectedly.
State at scale
Because React state is local, a common question comes up quickly: what happens when two distant components need the same value?
- Start by lifting the state up. Move the state to the nearest shared parent and pass it down as props. For most apps, this is enough.
- Use context when prop passing gets painful. If a value needs to reach many components several levels deep - the current user, a theme, a selected record - React's context lets any component read it without passing it through every layer. This is the closest match to Retool's "available everywhere" model.
- Reach for a library when the state gets complex. Tools like Redux help when many parts of an app read and update shared state frequently. For most internal tools, you won't need one - lifting state and context cover the majority of cases.
The trade-off is worth naming. Retool's global model is convenient, and for small apps built by one or two people, it's often all you need. But as an app grows, it becomes hard to tell what depends on what. Rename a variable or change the shape of a query's result, and a component on another page can quietly break - and you often won't find out until a user does. That's why React's approach suits larger apps that several developers maintain over time. React's approach takes more setup, but the data flow is visible in the code.
Front-end differences
Beyond state and data, a few things Retool handled quietly become your responsibility.
Routing. Multipage apps and URL parameters in Retool become routes in React. The new app builder uses React Router, where each page maps to a URL path and parameters are read with hooks like useParams.
Styling. Instead of the style panel, styles live in code, next to the components they apply to. Your app theme still sets the overall look, but fine-grained changes happen in the markup itself.
Components. In classic Retool, components are a fixed library you configure through the inspector. In the new app builder, they're source code. You can see how a component is built, and in many cases change it directly.
Debugging. Retool's debug panel shows you every query and variable in one place. In React, you'll lean on the browser's developer tools and the React DevTools extension, which let you inspect components, props, and state as the app runs.
The bottom line
Moving from Retool to React isn't learning a new set of ideas - it's learning to handle the ones you already use yourself. useState is your temporary state, useEffect is your query triggers, and useMemo is your transformer. The main shift is that nothing happens automatically: you decide where the state lives, when effects run, and how data is cached.
If you're an internal tools team planning to move your classic apps to the new builder, getting comfortable with React now means you can review what the converter produces and fix issues without waiting on outside help. If you're a Retool developer looking to take on more complex, custom work, reading React is what lets you shape the generated code instead of just accepting it. Either way, you don't need to write every line by hand - but when something goes wrong, understanding the code is what gets you unstuck.