![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
A React/Redux app is a one-way data flow cycle. The user interacts with the UI, which produces Redux actions, which get dispatched to reducer functions, which update the app’s state object, which gets passed back to React to re-render the UI.
A conversation with ChatGPT is also a one-way cycle. A human sends a prompt (action), which gets passed to the language model (reducer), which updates the conversation (state) with its response.
Can we use the latter to emulate the former? Spoilers: yes, we totally can! ChatGPT will happily act as your universal reducer, so you can prototype your app’s frontend without needing to mock up a backend or business logic right away. And after interacting with the frontend for a bit to teach the AI how the app works, you can even ask it to implement the reducer function for you.
Here’s a quick demo of a basic counter app with my react-redux-chatgpt
plugin doing the reducing (app on the left, behind-the-scenes ChatGPT conversation in the console on the right):
And here’s ChatGPT powering the Redux TodoMVC example, unmodified except to delete the reducers and replace them with ChatGPT:
Check out the source code on GitHub! Or keep reading for an in-depth explainer.
Basic Example: Counter
Let’s start with the counter app:
import { Provider, useSelector, useDispatch } from 'react-redux';
const initState = { count: 0 };
const App = () => {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<div>
{count}
counter:
<button onClick={() => dispatch({ type: 'increment' })}>
increment</button>
</div>
;
); }
Our initial state is just { count: 0 }
- can’t get more basic than that! Clicking the button will dispatch the action { type: 'increment' }
, and we expect our reducer to update the state to { count: 1 }
, { count: 2 }
, { count: 3 }
, etc.
Let’s pop over to ChatGPT and tell it what we’re doing:
ME: I’m a React/Redux app. I have a single data store.
Here’s the initial state of my store, in JSON form:
{"count":0}
AI: Great! It looks like you have a simple initial state for your Redux store, with a single property called “count” that has a value of 0.
In Redux, the store is the central place where the application’s state is stored. It is created by passing a reducer function to the
createStore
function. The reducer function is responsible for updating the state based on actions that are dispatched to the store.<snip>
Alright, cool. ChatGPT knows what Redux is. Can it process actions for us?
ME: My Redux reducers received an action, here encoded in JSON form:
{"type":"increment"}
What should the new state of my store be after this action?
AI: If your Redux reducer receives an action with a
type
of'increment'
, it should update the state of the store by incrementing the value of thecount
property by 1.<snip>
When the reducer function receives an action with a
type
of'increment'
, it will return a new state object with thecount
property incremented by 1. The rest of the state will be unchanged.For example, if the initial state of the store is
{ count: 0 }
, and the reducer function receives an action with atype
of'increment'
, the new state of the store will be{ count: 1 }
.It’s important to note that the reducer function should be a pure function, meaning that it should not have any side effects and should always return the same output for a given set of inputs. This makes it easier to test and reason about the behavior of your application.
The answer’s in there, but it’s buried in a bunch of explanation that isn’t machine-readable (…er, present AI company excluded). This is a common pattern with ChatGPT. Let’s try that again and see if we can make the AI get straight to the point.
ME: My Redux reducers received an action, here encoded in JSON form:
{"type":"increment"}
What should the new state of my store be after this action? Provide your answer in JSON form. Reply with only the answer in JSON form and include no other commentary.
AI:
{"count":1}
Perfect.
Now, we could start a fresh conversation every time we receive a new action, repeating our current state for the AI every time. But beyond costing more tokens when OpenAI finally starts charging for this magic, taking advantage of the ongoing conversation model that ChatGPT is built around helps keep its responses internally consistent. With the a little prompt engineering, we can get the AI to pull the current state from the conversation log, and guide its responses based on the previous actions and states it’s observed.
ME: I’m a React/Redux app. I have a single data store.
Here’s the initial state of my store, in JSON form:
{"count":0}
My Redux reducers received an action, here encoded in JSON form:
{"type":"increment"}
What should the new state of my store be after this action? Provide your answer in JSON form. Reply with only the answer in JSON form and include no other commentary.
AI:
{"count":1}
ME: My Redux reducers received an action, here encoded in JSON form:
{"type":"increment"}
Given the state of the store in your previous response, what should the new state of my store be after this action? Provide your answer in JSON form. Reply with only the answer in JSON form and include no other commentary.
AI:
{"count":2}
ME: My Redux reducers received an action, here encoded in JSON form:
{"type":"increment"}
Given the state of the store in your previous response, what should the new state of my store be after this action? Provide your answer in JSON form. Reply with only the answer in JSON form and include no other commentary.
AI:
{"count":3}
Now that we’ve got a cycle going, let’s experiment. Can ChatGPT synthesize actions for us?
ME: The user has requested that I do the following: add 3 to the count
What action(s) should I dispatch to my Redux store in order to complete this request? Provide your answer in the form of a JSON array of actions. Reply with only the answer in JSON array form and include no other commentary.
AI:
{"type":"increment","payload":3}
Okay, it’s made up a payload
parameter for the increment
action. Creative, but maybe not what we want. Can we get it to stick to using only what we’ve demonstrated so far?
ME: The user has requested that I do the following: add 3 to the count
Given the state of the store in your previous response and the types of actions I have received so far, what action(s) should I dispatch to my Redux store in order to complete this request? Provide your answer in the form of a JSON array of actions. Reply with only the answer in JSON array form and include no other commentary.
AI:
[{"type":"increment"},{"type":"increment"},{"type":"increment"}]
ME: My Redux reducers received some actions, here encoded in JSON form:
[{"type":"increment"},{"type":"increment"},{"type":"increment"}]
Given the state of the store in your previous response, what should the new state of my store be after this action? Provide your answer in JSON form. Reply with only the answer in JSON form and include no other commentary.
AI:
{"count":6}
Sweet. It can even handle reducing a sequence of actions with a single prompt. That’ll save on round-trips to OpenAI.
We can cut out the AI entirely once we’ve demonstrated to it how the flow should go. It’s happy to synthesize a reducer for us:
ME: Give me JavaScript code that implements my reducer function. It should be able to handle all of the actions I told you I received, producing the states you told me would result from applying them.
AI: Sure, here is a JavaScript code that implements a reducer function that can handle the
increment
action:const reducer = (state = {count: 0}, action) => { switch (action.type) { case 'increment': return {count: state.count + 1}; default: return state; }; }
This reducer function will initialize the state to
{count: 0}
if no state is provided. When it receives anincrement
action, it will return a new state object with the count increased by 1. If it receives any other action type, it will simply return the current state.You can add additional actions by adding new cases to the
switch
statement. For example, if you wanted to handle adecrement
action, you could add a case for it like this:case 'decrement': return {count: state.count - 1};
I hope this helps! Let me know if you have any questions.
There’s no official API for ChatGPT yet, so I’m using the unofficial chatgpt
NPM package to wire up the app in the demo video. OpenAI has their Cloudflare protection cranked up high; to get around that, it runs a full Chrome instance (!) in the background to shuttle messages back and forth between the little REST API server I wrote and ChatGPT’s internal API.
The logic for guiding ChatGPT through a conversation like the above is packed into my client-side react-redux-chatgpt
package. It constructs the prompts, keeps track of conversation threads, and can even automatically retry prompts in the rare case ChatGPT spits out malformed data. There’s also a few stock components bundled in for implementing the “thinking…” screen and action bar in the demo, and methods like synthesizeReducers
and synthesizeActions
for generating code. Hooking it up to our counter app is as simple as:
import { createRoot } from 'react-dom/client';
import { AiButtons, ChatGptBackend, ThinkingScreen } from 'react-redux-chatgpt';
const backend = new ChatGptBackend(initState);
const root = createRoot(document.getElementById('root'));
root.render(
<Provider store={backend.store}>
<App />
<AiButtons backend={backend} />
<ThinkingScreen useSelector={useSelector} />
</Provider>
; )
Advanced Example: TodoMVC
So ChatGPT can handle a counter app. Big whoop. How about something more substantial?
TodoMVC is a standard benchmark for web frameworks: the same app, with the same interface, implemented for React, Mithril, Vue, Elm, Angular… The task is to implement a todo list, as the name suggests, with the ability to add todo items, mark them completed, filter based on completion status, and all clear completed items with a click.
I’ve downloaded the React/Redux version and deleted the reducers/
directory. Bye bye, business logic! The main JS module looks like this out of the box:
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './components/App'
import reducer from './reducers'
import 'todomvc-app-css/index.css'
const store = createStore(reducer)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
And this is what it looks like after tossing the reducers and dropping in react-redux-chatgpt
:
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider, useSelector } from 'react-redux'
import { AiButtons, ChatGptBackend, ThinkingScreen } from 'react-redux-chatgpt';
import App from './components/App'
import 'todomvc-app-css/index.css'
const initState = {
todos: [{ text: 'Use Redux', completed: false, id: 0 }],
visibilityFilter: 'show_all'
}const backend = new ChatGptBackend(initState, { description: 'todo list' })
render(
<Provider store={backend.store}>
<App />
<AiButtons backend={backend} />
<ThinkingScreen useSelector={useSelector} />
</Provider>,
document.getElementById('root')
)
That’s the full extent of the changes I had to make to get this to work. Note I’ve dropped the AI a hint about what the app is for with the description: 'todo list'
option passed to ChatGptBackend
.
Let’s take a peek at the conversation that happens under the hood as we interact with the app. Clicking the circle next to a todo item does this:
ME: I’m a React/Redux app. I have a single data store.
A description of the app is: todo list
Here’s the initial state of my store, in JSON form:
{"todos":[{"text":"Use Redux","completed":false,"id":0}],"visibilityFilter":"show_all"}
My Redux reducers received an action, here encoded in JSON form:
{"type":"COMPLETE_TODO","id":0}
What should the new state of my store be after this action? Provide your answer in JSON form. Reply with only the answer in JSON form and include no other commentary.
AI:
{"todos":[{"text":"Use Redux","completed":true,"id":0}],"visibilityFilter":"show_all"}
It set the completed
property to true
on todo 0. Good job, bot! Here’s the UI after the re-render:
Punch “Read more books” into the text field and hit return, and this happens:
ME: My Redux reducers received an action, here encoded in JSON form:
{"type":"ADD_TODO","text":"Read more books"}
Given the state of the store in your previous response, what should the new state of my store be after this action? Provide your answer in JSON form. Reply with only the answer in JSON form and include no other commentary.
AI:
{"todos":[{"text":"Use Redux","completed":true,"id":0},{"text":"Read more books","completed":false,"id":1}],"visibilityFilter":"show_all"}
It even figured out that todo items need unique id
s! And the new item starts with "completed": false
, while the previous one is left at "completed": true
.
We can use the filter links:
ME: My Redux reducers received an action, here encoded in JSON form:
{"type":"SET_VISIBILITY_FILTER","filter":"show_completed"}
Given the state of the store in your previous response, what should the new state of my store be after this action? Provide your answer in JSON form. Reply with only the answer in JSON form and include no other commentary.
AI:
{"todos":[{"text":"Use Redux","completed":true,"id":0},{"text":"Read more books","completed":false,"id":1}],"visibilityFilter":"show_completed"}
And we can “clear completed”:
ME: My Redux reducers received an action, here encoded in JSON form:
{"type":"CLEAR_COMPLETED"}
Given the state of the store in your previous response, what should the new state of my store be after this action? Provide your answer in JSON form. Reply with only the answer in JSON form and include no other commentary.
AI:
{"todos":[{"text":"Read more books","completed":false,"id":1}],"visibilityFilter":"show_all"}
We can go freeform and tell ChatGPT to dream up actions with natural language, then execute them all at once:
const actions = await backend.synthesizeActions("add todo items for reading each of Tokein's books")
// [
// {type: 'ADD_TODO', text: 'The Hobbit'},
// {type: 'ADD_TODO', text: 'The Lord of the Rings'},
// {type: 'ADD_TODO', text: 'The Silmarillion'},
// {type: 'ADD_TODO', text: 'The Children of Hurin'},
// {type: 'ADD_TODO', text: 'The Fall of Gondolin'},
// {type: 'ADD_TODO', text: 'The Adventures of Tom Bombadil'}
// ]
.store.dispatch(actions) backend
And because we’re dealing with an all-knowing AI, we can make up our own actions that go beyond the standard TodoMVC set:
.store.dispatch({type: 'COMPLETE_ALL_TODOS'}) backend
After all that, ChatGPT can spit out a reducer function that handles the whole app, no sweat:
await backend.synthesizeReducers()
Here is an example of how you could implement the reducer function in JavaScript to handle the actions you described:
const todoReducer = (state, action) => { switch (action.type) { case 'ADD_TODO': return { todos: [ ...state.todos, {text: action.text, completed: false, id: state.todos.length }, ]visibilityFilter: state.visibilityFilter ; }case 'COMPLETE_TODO': return { todos: state.todos.map(todo => .id === action.id ? { ...todo, completed: true } : todo todo, )visibilityFilter: state.visibilityFilter ; }case 'SET_VISIBILITY_FILTER': return { todos: state.todos, visibilityFilter: action.filter ; }case 'CLEAR_COMPLETED': return { todos: state.todos.filter(todo => !todo.completed), visibilityFilter: state.visibilityFilter ; }case 'COMPLETE_ALL_TODOS': return { todos: state.todos.map(todo => ({ ...todo, completed: true })), visibilityFilter: state.visibilityFilter ; }default: return state; }; }
This reducer function takes in the current state of the store and an action, and returns the new state of the store based on the action type. It handles all of the actions you described and produces the states that you specified.
Note that this is just one way to implement the reducer function, and there are many other ways you could do it as well. The important thing is to make sure that the reducer function correctly handles all of the actions that your application can dispatch, and produces the correct new state for the store.
The future of coding is here, and it’s chatting with us.
P.S. Wondering if ChatGPT can generate the frontend code, too? The answer is yes - sort of! Sometimes it forgets to include an input field for the todo text.
P.P.S. I’m looking for PhD research internships this summer! Let me know if you have any leads.