Skip to content

React Integration Example

Complete React integration examples with the useBindra custom hook.

Installation

bash
npm install bindra react react-dom
# or
pnpm add bindra react react-dom

Custom Hook: useBindra

The useBindra hook provides a React-friendly interface to Bindra DataSource:

typescript
function useBindra<T>(config: DataSourceConfig) {
  return {
    data: T[],           // Reactive data array
    current: T | null,   // Current record
    loading: boolean,    // Loading state
    error: Error | null, // Error state
    create: (record) => Promise<T>,
    update: (id, changes) => Promise<T>,
    remove: (id) => Promise<void>,
    refresh: () => Promise<void>,
    dataSource: DataSource<T>
  };
}

Examples

1. Todo List

tsx
import { useBindra } from './hooks/useBindra';

function TodoList() {
  const { data, loading, create, update, remove } = useBindra<Todo>({
    url: '/api/todos',
    cache: { enabled: true, ttl: 300000 }
  });

  const handleToggle = (todo: Todo) => {
    update(todo.id, { completed: !todo.completed });
  };

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => handleToggle(todo)}
          />
          {todo.title}
          <button onClick={() => remove(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

2. Searchable Product List with Pagination

tsx
function ProductList() {
  const { data, loading, dataSource } = useBindra({
    url: '/api/products',
    pagination: { enabled: true, pageSize: 20 }
  });

  const debouncedSearch = useMemo(
    () => debounce((query: string) => {
      dataSource.fetch({ search: query });
    }, 300),
    []
  );

  return (
    <div>
      <input
        type="search"
        onChange={(e) => debouncedSearch(e.target.value)}
      />
      
      <div className="products">
        {data.map(product => (
          <ProductCard key={product.id} {...product} />
        ))}
      </div>
      
      {dataSource.hasMore && (
        <button onClick={() => dataSource.fetchMore()}>
          Load More
        </button>
      )}
    </div>
  );
}

3. Real-time Chat

tsx
function Chat() {
  const { data: messages, create } = useBindra({
    url: '/api/messages',
    realtime: {
      enabled: true,
      url: 'wss://api.example.com/ws'
    }
  });

  const [message, setMessage] = useState('');

  const send = () => {
    create({ text: message, userId: currentUser.id });
    setMessage('');
  };

  return (
    <div>
      <div className="messages">
        {messages.map(msg => (
          <Message key={msg.id} {...msg} />
        ))}
      </div>
      <input
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && send()}
      />
    </div>
  );
}

4. Form with Validation

tsx
function UserForm() {
  const { create, loading } = useBindra({
    url: '/api/users',
    validation: {
      name: [
        { type: 'required', message: 'Name required' },
        { type: 'minLength', value: 3 }
      ],
      email: [
        { type: 'required' },
        { type: 'email' }
      ]
    },
    security: {
      sanitizeFields: ['name', 'bio']
    }
  });

  const [data, setData] = useState({ name: '', email: '' });
  const [errors, setErrors] = useState({});

  const submit = async (e) => {
    e.preventDefault();
    try {
      await create(data);
      setData({ name: '', email: '' });
    } catch (err) {
      setErrors(err.validationErrors || {});
    }
  };

  return (
    <form onSubmit={submit}>
      <input
        value={data.name}
        onChange={(e) => setData({...data, name: e.target.value})}
      />
      {errors.name && <span>{errors.name}</span>}
      
      <button disabled={loading}>Submit</button>
    </form>
  );
}

Features

Automatic Cleanup

The useBindra hook automatically cleans up subscriptions and WebSocket connections when the component unmounts.

Reactive State

All state updates are automatic - no manual subscriptions needed:

tsx
const { data, current, loading } = useBindra(config);

// data, current, and loading update automatically
// when DataSource changes

TypeScript Support

Full type safety with generics:

tsx
interface User {
  id: number;
  name: string;
  email: string;
}

const { data, create, update } = useBindra<User>({
  url: '/api/users'
});

// data is User[]
// create/update are type-safe

Best Practices

  1. Memoize DataSource config

    tsx
    const config = useMemo(() => ({
      url: '/api/users',
      cache: { enabled: true }
    }), []);
  2. Debounce search inputs

    tsx
    const search = useMemo(
      () => debounce((q) => ds.fetch({ search: q }), 300),
      []
    );
  3. Handle errors

    tsx
    const { error } = useBindra(config);
    
    if (error) {
      return <ErrorMessage error={error} />;
    }
  4. Show loading states

    tsx
    const { loading } = useBindra(config);
    
    return loading ? <Spinner /> : <DataList />;

Learn More

Released under the MIT License.