Skip to main content

React function calling tutorial: Tool-using AI agents

Function calling transforms a chatbot from a pure language model into an AI agent—a system that can invoke tools, access external data, and perform actions. Instead of only generating text, the AI can call functions you define (e.g., get_weather, search_database, send_email) and receive their results. This article covers defining tools, handling function calls in React, and integrating results back into the conversation.

According to Anthropic's 2025 AI Agent study, 71% of production chatbots use function calling to fetch real-time data or perform actions. It is no longer optional; it is the foundation of useful AI assistants.

Defining Tools for Your AI

Begin by defining the tools your AI can use. Each tool needs a name, description, and parameter schema (in JSON Schema format):

// Define tools your AI can invoke
const tools = [
{
name: 'get_weather',
description: 'Get the current weather for a location',
input_schema: {
type: 'object',
properties: {
location: {
type: 'string',
description: 'The city and state, e.g. San Francisco, CA',
},
unit: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: 'Temperature unit',
},
},
required: ['location'],
},
},
{
name: 'search_documents',
description: 'Search your document database for relevant passages',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The search query',
},
},
required: ['query'],
},
},
];

// Implementations for each tool
const toolFunctions = {
get_weather: async ({ location, unit = 'fahrenheit' }) => {
// Call a weather API (OpenWeatherMap, etc.)
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${location}&units=${unit}&appid=${apiKey}`
);
const data = await response.json();
return {
temperature: data.main.temp,
condition: data.weather[0].main,
humidity: data.main.humidity,
};
},

search_documents: async ({ query }) => {
// Search your backend or vector database
const response = await fetch('https://yourbackend.com/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
});
const results = await response.json();
return { passages: results.slice(0, 5) }; // Top 5 results
},
};

export { tools, toolFunctions };

JSON Schema is the standard format for defining tool parameters. It describes the type, required fields, and constraints for each parameter. The AI uses this to generate valid function calls.

Building the Function-Calling Chat Loop

Now implement the agentic loop: send the tools to the API, receive function calls, invoke them, and feed results back:

import React, { useState } from 'react';
import { tools, toolFunctions } from './tools';

function FunctionCallingChat() {
const [messages, setMessages] = useState([]);
const [isLoading, setIsLoading] = useState(false);

const handleSendMessage = async (userMessage) => {
// Add user message
setMessages((prev) => [...prev, { role: 'user', content: userMessage }]);
setIsLoading(true);

// Conversation loop: keep calling the API until it stops using tools
let conversationMessages = messages.concat({ role: 'user', content: userMessage });
let continueLoop = true;

while (continueLoop && isLoading) {
try {
// Call API with tools enabled
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: conversationMessages,
tools: tools.map((tool) => ({
type: 'function',
function: tool,
})),
tool_choice: 'auto', // Let the model decide when to use tools
}),
});

const data = await response.json();
const assistantMessage = data.choices[0].message;

// Add assistant's message (may contain tool calls)
conversationMessages = conversationMessages.concat(assistantMessage);
setMessages((prev) => [
...prev,
{ role: 'assistant', content: assistantMessage.content || '' },
]);

// Check if the assistant wants to use a tool
if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) {
// Process each tool call
const toolResults = [];

for (const toolCall of assistantMessage.tool_calls) {
const toolName = toolCall.function.name;
const toolArgs = JSON.parse(toolCall.function.arguments);

console.log(`Invoking ${toolName} with args:`, toolArgs);

try {
// Call the tool function
const result = await toolFunctions[toolName](toolArgs);
toolResults.push({
type: 'tool_result',
tool_use_id: toolCall.id,
content: JSON.stringify(result),
});

// Display tool result in chat
setMessages((prev) => [
...prev,
{
role: 'system',
content: `Tool result from ${toolName}: ${JSON.stringify(result)}`,
},
]);
} catch (error) {
toolResults.push({
type: 'tool_result',
tool_use_id: toolCall.id,
content: `Error: ${error.message}`,
is_error: true,
});

setMessages((prev) => [
...prev,
{ role: 'system', content: `Error calling ${toolName}: ${error.message}` },
]);
}
}

// Add tool results to the conversation and loop again
conversationMessages = conversationMessages.concat({
role: 'user',
content: toolResults,
});
} else {
// No more tool calls; the assistant has finished
continueLoop = false;
}
} catch (error) {
console.error('API error:', error);
setMessages((prev) => [
...prev,
{ role: 'system', content: `Error: ${error.message}` },
]);
continueLoop = false;
}
}

setIsLoading(false);
};

return (
<div>
<div style={{ height: '400px', overflowY: 'auto', marginBottom: '16px' }}>
{messages.map((msg, idx) => (
<div key={idx} style={{ marginBottom: '8px', padding: '8px', backgroundColor: msg.role === 'user' ? '#e3f2fd' : '#f5f5f5' }}>
<strong>{msg.role}:</strong> {msg.content}
</div>
))}
</div>
<input
type="text"
placeholder="Ask me anything..."
onKeyDown={(e) => {
if (e.key === 'Enter' && e.target.value && !isLoading) {
handleSendMessage(e.target.value);
e.target.value = '';
}
}}
disabled={isLoading}
/>
</div>
);
}

export default FunctionCallingChat;

The key flow: send the tools in the API request, check if the response contains tool_calls, invoke the tools, and feed the results back to the AI. The AI then continues from where it left off, possibly using more tools or providing a final answer.

Displaying Tool Usage in the UI

Show users which tools the AI is using and their results:

function ChatMessage({ msg }) {
if (msg.role === 'tool_call') {
return (
<div style={{
padding: '8px',
backgroundColor: '#fff3e0',
borderLeft: '4px solid #ff9800',
marginBottom: '8px',
}}>
<strong>Calling tool: {msg.toolName}</strong>
<pre style={{ fontSize: '12px', marginTop: '4px' }}>
{JSON.stringify(msg.args, null, 2)}
</pre>
</div>
);
}

if (msg.role === 'tool_result') {
return (
<div style={{
padding: '8px',
backgroundColor: '#e8f5e9',
borderLeft: '4px solid #4caf50',
marginBottom: '8px',
}}>
<strong>Result from {msg.toolName}:</strong>
<pre style={{ fontSize: '12px', marginTop: '4px' }}>
{JSON.stringify(msg.result, null, 2)}
</pre>
</div>
);
}

return (
<div style={{
padding: '8px',
backgroundColor: msg.role === 'user' ? '#e3f2fd' : '#f5f5f5',
marginBottom: '8px',
}}>
<strong>{msg.role}:</strong> {msg.content}
</div>
);
}

This provides transparency: users can see exactly which tools the AI invoked and what data it received.

Handling Tool Errors Gracefully

If a tool fails, pass the error back to the AI so it can retry, use a different tool, or explain the issue to the user:

try {
const result = await toolFunctions[toolName](toolArgs);
toolResults.push({
type: 'tool_result',
tool_use_id: toolCall.id,
content: JSON.stringify(result),
});
} catch (error) {
// Pass the error back to the AI
toolResults.push({
type: 'tool_result',
tool_use_id: toolCall.id,
content: `Error: ${error.message}. The database is temporarily unavailable. Please try again later.`,
is_error: true,
});
}

The AI will receive the error and can decide how to respond—it might retry with different parameters, suggest an alternative, or explain the failure to the user.

Tool Capabilities Comparison

CapabilityOpenAIAnthropicGemini
Tool calls in streamingYesYesYes
Parallel tool callsYes (multiple per turn)YesLimited
Tool result formatJSONJSONJSON
Error handlingSupportedSupportedSupported
Max tools per request128Unlimited128

All major AI APIs support function calling. The patterns in this article work with any of them; adjust the API endpoint and message format as needed.

Key Takeaways

  • Define tools with JSON Schema describing parameters and constraints.
  • Implement the agentic loop: send tools, check for tool calls, invoke them, feed results back.
  • Display tool calls and results in the UI to provide transparency to users.
  • Pass errors from tools back to the AI so it can retry or respond gracefully.
  • Use tool_choice: 'auto' to let the AI decide when to use tools.

Frequently Asked Questions

What is the difference between function calling and regular text generation?

Function calling allows the AI to invoke functions you define and receive their results. Regular text generation only produces text. Function calling makes the AI an agent that can access real-time data and perform actions.

Can the AI use multiple tools in one turn?

Yes. OpenAI allows multiple tool_calls in a single response. Process all of them, gather results, and feed them back to the AI in one message.

Should I validate tool parameters before invoking?

Yes. Check that the arguments match the schema (correct types, required fields present). This prevents runtime errors and gives the AI feedback to correct itself.

How do I prevent infinite tool call loops?

Set a maximum number of iterations (e.g., 10) before exiting the loop. If the AI keeps calling the same tool with invalid parameters, break out and explain the issue to the user.

Can I define dynamic tools based on user input?

Yes. Build the tools array at runtime based on the user's configuration or permissions. Some users might have access to sensitive tools that others do not.

Further Reading