Create your own
Lesson illustration

Designing Input Forms

Hello! Welcome back.

In our last lesson, we connected our dApp to the blockchain to read data, successfully fetching and displaying a user's native SOL and SPL token balances. This was a huge step in making our application dynamic and user-aware.

Today, we shift our focus from reading to writing. Before we can send a transaction to change the state of the blockchain, we first need to gather the necessary information from the user. This lesson is dedicated to exactly that: building a UI form to gather user input for a program instruction. Your extensive experience with React and frontend development will be a great asset here, as we'll be applying familiar patterns to the unique context of Web3.

The Foundation: Controlled Components for Program Instructions

In React, the standard way to handle form data is through controlled components. As you know, this means the component's state is the "single source of truth" for the input elements. We'll use this exact pattern to collect the parameters required by our Solana program's instructions.

Imagine a simple program instruction in Anchor that looks like this:

pub fn update_message(ctx: Context<Update>, new_message: String) -> ProgramResult { ... }

This instruction requires one argument: new_message of type String. Our goal on the frontend is to create a form with a text input where the user can type this message. The value from this input will then be passed directly into our transaction call.

Let's see this in action. The following article provides a clear, full-stack example. We'll focus on the React client code.

The Complete Guide to Full Stack Solana Development ...

This guide by Nader Dabit demonstrates a simple Solana dApp. We'll examine the second example, which allows a user to submit a message to the blockchain.

Please read the section titled 'Hello World part 2'. Focus on the React code in the file /* app/src/App.js */. Pay close attention to these three parts: The useState hook for the input: const [input, setInput] = useState(''); The JSX for the input field and button: <input ... onChange={e => setInput(e.target.value)} ... /> and <button onClick={update}> The update function, which uses the input state variable when calling the program: await program.rpc.update(input, { ... });

As you can see from the article, the process is straightforward and should feel very familiar:

  1. State: A useState hook (const [input, setInput] = useState('')) holds the value of the form input.
  2. UI: An <input> element's value is bound to the input state, and its onChange handler calls setInput to update the state on every keystroke.
  3. Submission: A <button>'s onClick handler calls an update function.
  4. Integration: Inside the update function, the input state variable is passed directly as an argument to the Anchor program.rpc.update() method.

This directly connects the user's input in the browser to the data field of a Solana instruction.

Handling Forms with Multiple Inputs

Most program instructions are more complex and require multiple arguments. For example:

pub fn create_listing(ctx: Context<Create>, item_name: String, price: u64) -> ProgramResult { ... }

While you could use a separate useState for each field (itemName, price), this can become cumbersome. A more scalable and elegant pattern, common in advanced React development, is to use a single state object and a generic handleChange function.

This video provides an excellent, concise tutorial on this exact pattern. It's not Web3-specific, which highlights that you are applying a best-practice React pattern to a new domain.

How to Use a Single Function to Manage React Form State

This video by Dave Gray explains how to manage form state with a single handler, which is perfect for our use case where program instructions often have multiple parameters.

Watch the sections on setting up the state object, creating the generic handleChange function, and integrating it with the inputs (timestamps 02:19-03:16 and 04:24-08:47). Notice how the function uses e.target.name to dynamically update the correct property in the state object. This is the key to its reusability.

By adopting this pattern, your form state object can be structured to mirror the arguments of your program instruction.

const [formData, setFormData] = useState({
  itemName: '',
  price: 0,
});

const handleChange = (e) => {
  const { name, value } = e.target;
  setFormData(prevData => ({
    ...prevData,
    [name]: value,
  }));
};

// And in your JSX:
// <input name="itemName" value={formData.itemName} onChange={handleChange} />
// <input type="number" name="price" value={formData.price} onChange={handleChange} />

When you call your program, you can then destructure the state object:

await program.rpc.createListing(formData.itemName, formData.price, { ... });

A Practical Example in a Next.js App

Let's look at another practical implementation. This article uses Next.js and demonstrates a clean separation of concerns, where the UI component gathers the data and passes it to a dedicated function for processing.

The Complete Guide to Full Stack Solana Development ...

This Next.js-based guide shows a slightly different but very effective structure for handling user input and submitting it to the program.

Read the code block for the app/pages/index.tsx file within the section 'Making a Create Message API'. Identify the inputtedMessage state variable. See how the onChange handler of the <input> element updates this state. Observe that the button's onClick handler calls a separate createMessage function, passing the inputtedMessage state as an argument. This is a great pattern for keeping your components clean.

This approach, where the component's responsibility is primarily to manage its UI state and then delegate the complex web3 logic to another function, is excellent for maintainability and testing, especially as your dApp grows.

Finally, let's watch one more example that puts all the pieces together in a live demonstration.

Solana Tutorial: Writing a dApp to Work With PDA's | React, Next.js, Anchor

This video from Josh's DevBox walks through building a dApp to interact with a PDA. We'll focus on the part where he wires up the UI.

Watch the section from 00:16:10 to 00:24:19. The key part is the review of the index.tsx file. He points out the 'Create Transaction' button and its onClick handler, which calls the sendTransaction function. Inside that function, you can see how the transaction is constructed. Although the instruction arguments are hardcoded in this example, it clearly demonstrates the event-driven flow: a user clicks a button, which triggers a function that prepares and sends the transaction.

Test your understanding!

Imagine your Solana program has an instruction create_profile that takes two arguments: username (a string) and age (an unsigned 8-bit integer).

Using the generic handleChange pattern from the Dave Gray video, what would your initial useState call look like? How would you need to modify the handleChange function to ensure the age is stored as a number and not a string?

Show answer

Your initial state would be an object:

const [profileData, setProfileData] = useState({
  username: '',
  age: 0,
});

To handle the age as a number, you would add a check inside handleChange based on the input's type or name.

const handleChange = (e) => {
  const { name, value, type } = e.target;
  
  // Parse the value as a number if the input type is 'number'
  const processedValue = type === 'number' ? parseInt(value, 10) : value;

  setProfileData(prevData => ({
    ...prevData,
    [name]: processedValue,
  }));
};

// Your JSX would look like this:
// <input name="username" type="text" value={profileData.username} onChange={handleChange} />
// <input name="age" type="number" value={profileData.age} onChange={handleChange} />

This ensures the age field in your state object is a number, ready to be sent to the program which expects an integer.

Conclusion

In this lesson, you've learned how to apply your existing frontend expertise to build the essential user interface for interacting with Solana programs.

Key Takeaways:

  • Controlled Components are Key: Standard React form patterns are the foundation for gathering user input for Solana instructions.
  • State Management Strategy: For simple forms, a single useState hook is sufficient. For more complex forms with multiple inputs, using a single state object and a generic handleChange function is a more scalable and maintainable pattern.
  • Connecting UI to Logic: The data collected in your form's state is passed directly as arguments to the Anchor program.rpc.instructionName() function.
  • Separation of Concerns: For cleaner code, you can keep transaction logic in separate functions that are called from your component's event handlers (like onClick), passing the form state as parameters.

Preview of the Next Lesson:

We've now built the form to gather user input. The next logical step is to take that input and use it to execute a transaction on the blockchain. In our next lesson, "Construct and submit a program transaction based on user input from the UI," we will do just that. We'll take the form state, build a transaction object, have the user sign it with their wallet, and submit it to the Solana network.

Can't find a good explanation? Sign up and we'll make it for you

Sign up