2024 06 14 implementing commandcontext in react for efficient command execution and context access - bizkite-co/handterm GitHub Wiki


original file name: Codeium Chat - A00yBCQt860fNozXHLqSsCEo1vRiDHHu.md summary: The chat conversation involved a user seeking advice on implementing a CommandContext in React and handling accessing context within command execution. Here are the key points discussed:

  1. The initial implementation of ICommandContext had a self-reference issue due to the executeCommand method taking a context parameter.
  2. Suggestions were provided to refactor ICommandContext to remove the self-reference, allowing access to context using useContext or contextType.
  3. A discussion on accessing HandTerm context within commands by passing the context as an argument or binding it when calling the execute method.
  4. Refactoring suggestions were given to structure commands as methods of HandTerm or using a higher-order component to inject context.
  5. The importance of avoiding prop drilling by utilizing React's Context API was emphasized.
  6. Suggestions were provided on how to structure the application for adding commands as plugins and handling output manipulation within the HandTerm and CommandContext.
  7. The proposed structure included managing output state and functions within HandTerm and using CommandContext to provide functions to append to and clear the output.

Overall, the chat focused on guiding the user towards modern React best practices for managing context, command execution, output manipulation, and application structure. It aimed to help the user create a clean and efficient system for adding commands and interacting with the HandTerm component in their TUI CLI application. date: 2024-06-14T06:36

Codeium Chat Conversation

Note: This is purely the output of the chat conversation and does not contain any raw data, codebase snippets, etc. used to generate the output.

User at 6:36 am, Jun 14, 2024

I have implemented a CommandContext with React.createContext.

It seems to have a self-referrence.

I am new to React and I want to get your espert opinion on best practices and code smells.

Does this look right to you?

// CommandContext.tsx
import React from 'react';

export interface ICommandContext {
 commandHistory: string[];
 setCommandHistory: React.Dispatch<React.SetStateAction<string[]>>;
 executeCommand: (
  commandName: string, 
  args?: string[], 
  switches?: Record<string, boolean | string>,
  context?: ICommandContext
 ) => string;
}

// Create the context with a default value
export const CommandContext = React.createContext<ICommandContext | null>(null);

I think I need to be able to pass the context in to the command so that I can run commands from the context of HandTerm

  if(this.context) {
   const args = [''];
   const switchs = {}
   const output = this.context
    .executeCommand(
     command, 
     args, 
     switchs,
     this.context
    );
   console.log('Command output', output);
  }

Codeium at 6:37 am, Jun 14, 2024

In the TypeScript definition for ICommandContext, you have a method executeCommand which takes an optional context argument of type ICommandContext. This creates a circular reference since ICommandContext references itself within its own definition.

While React's Context does allow you to pass around the context itself as a value, it's not common to include the context object within the executeCommand method signature. Instead, the context is typically consumed within components or hooks that need it.

With the current ICommandContext interface, you have a method that looks like it should execute a command, but it also takes the context as a parameter, which is unconventional and could indeed be considered a code smell. It's not necessary for the executeCommand method to take the context as a parameter because any command that is executed within a React component that has access to the CommandContext can also consume the context directly using useContext or contextType.

Here's how you might refactor the ICommandContext interface without the self-reference:

// CommandContext.tsx
import React from 'react';

export interface ICommandContext {
  commandHistory: string[];
  setCommandHistory: React.Dispatch<React.SetStateAction<string[]>>;
  executeCommand: (
    commandName: string,
    args?: string[],
    switches?: Record<string, boolean | string>
  ) => string;
}

// Create the context with a default value
export const CommandContext = React.createContext<ICommandContext | null>(null);

And then, when you want to use the executeCommand method in a component, you can consume the context without passing it as a parameter:

if (this.context) {
  const args = [''];
  const switches = {};
  // You can now call executeCommand directly without passing the context
  const output = this.context.executeCommand(command, args, switches);
  console.log('Command output', output);
}

In this case, since you are within a class component that has already accessed this.context, there's no need to pass the context back into executeCommand. The method will have access to this.context if it needs it, assuming it's defined within the same scope that has context access.


User at 8:52 am, Jun 14, 2024

I want to access the context of HandTerm when I am in this fuction that is called by HandTerm:

import Phrases from '../utils/Phrases';
import { ICommandContext } from './CommandContext';
import { ICommand } from './ICommand';

export const ListPhrasesCommand: ICommand = {
 name: 'ls',
 description: 'List files',
 switches: {
  'all': 'List all phrases',
  'random': 'List a random phrase',
  'easy': 'List only easy phrases',
 },
 execute: (
  _commandName: string, 
  _args?: string[], 
  _switches?: Record<string, boolean | string>, 
 ) => {
  if (!_commandName) {
   return 'No command context available.';
  }
  // Logic to clear the command history from localStorage
  // Logic to clear the command history from context (state)
  const phrases = Phrases.getPhrases().join('\n');
  return phrases;
 }
};

I useing it within the CommandContext, I think, but I am not sure how to verify that other than looking at the code.

When I put a breakpoint in ListPhrasesCommand.execute() code block, the this is undefined, but I think it might be available just in the outer context.

I need to reference HandTerm context from ListPhraseCommand.execute()


Codeium at 8:52 am, Jun 14, 2024

In React, the value of this inside a function depends on how the function is called, not where it is defined. If this is undefined within ListPhrasesCommand.execute(), it's likely because the function is being called without the proper context binding.

When execute is called from within HandTerm, you will need to ensure that it has a reference to the context of HandTerm. One way to do this is by explicitly passing the context as an argument to the execute method. However, since execute is defined as a property of an object and not as a method in a class, it won't automatically have access to this referring to the HandTerm instance.

To solve this, you can either:

  1. Bind the context when you call execute from HandTerm.
  2. Pass the necessary data as arguments to the execute function.

Here's an example of passing the context as an argument:

// In ICommand interface definition
interface ICommand {
  // ...other properties
  execute: (
    commandName: string, 
    args?: string[], 
    switches?: Record<string, boolean | string>,
    handTermContext?: HandexTerm // Type this according to HandexTerm's type
  ) => string;
}

// HandTerm.tsx (or wherever you call execute)
if (this.context) {
  const args = [''];
  const switches = {};
  // Pass `this` which is the context of HandTerm to the execute function
  const output = this.context.executeCommand(command, args, switches, this);
  console.log('Command output', output);
}

// ListPhrasesCommand.tsx
export const ListPhrasesCommand: ICommand = {
  // ...other properties
  execute: (
    _commandName: string, 
    _args?: string[], 
    _switches?: Record<string, boolean | string>, 
    handTermContext?: HandexTerm // Add a parameter for HandexTerm context
  ) => {
    if (!handTermContext) {
      return 'No HandTerm context available.';
    }
    // You can now use handTermContext to access HandTerm's methods and state
    const phrases = Phrases.getPhrases().join('\n');
    return phrases;
  }
};

Please ensure you replace HandexTerm with the correct type or interface that represents the context of HandTerm.

This approach modifies the command execution pattern to include the context of HandTerm, but it also increases coupling. A cleaner approach could be to refactor the commands to be methods of HandTerm itself, or have them call methods on HandTerm that are passed down as callbacks. This approach would allow better encapsulation of the context and would follow React's data flow principles more closely.


User at 9:01 am, Jun 14, 2024

So, I've implemented a CommandContext according to React.createContext and I've wrapped the whole app like this in App.tsx:

 return (
  <CommandProvider >
   <div ref={containerRef}>
    <HandexTerm
     ref={handexTermRef}
     terminalWidth={containerWidth}
    />
   </div>
  </CommandProvider>
 );

But you're saying I now have to pass a HandTerm reference down in every funciton call and add it to the ICommand definition?

Couldn't I have done that without implrementing the whole context?


Codeium at 9:02 am, Jun 14, 2024

You're right—passing references down through each function call can be cumbersome and can negate some of the benefits of using React's Context API, which is designed to provide a way to share values like these across components without having to explicitly pass a prop through every level of the tree.

The purpose of the Context API is to avoid "prop drilling" (the process you described of passing props through many layers). Normally, context is used within components that are children of the Provider, typically accessed using the useContext Hook for functional components or contextType for class components.

To access HandTerm context within your commands without passing it in every function call, you could:

  1. Make commands methods of HandTerm. This way, they can naturally access this.context.

  2. Use a higher-order component or custom hook to inject context into your commands.

  3. Modify the command execution logic to provide a bound context to the command functions.

Here's an example of how you might modify the context provider to allow commands to access the necessary state or functions from HandTerm:

// CommandProvider.tsx

// ... other imports
import { ICommand } from './ICommand';

// This is a simplified example of what the CommandProvider might look like
export const CommandProvider: React.FC<{ handTermRef: React.RefObject<HandTerm> }> = ({ children, handTermRef }) => {
  // ... other state and logic

  const executeCommand = (commandName: string, args?: string[], switches?: Record<string, boolean | string>) => {
    // Access HandTerm methods or state using the ref
    const handTerm = handTermRef.current;

    // Ensure that handTerm is not null
    if (!handTerm) {
      console.error('HandTerm reference is null');
      return 'Execution context is not available';
    }

    // Get the command from the registry
    const command = commandRegistry.getCommand(commandName);
    if (command) {
      // Execute the command with the necessary context
      return command.execute(args, switches, handTerm);
    }

    return `Command not found: ${commandName}`;
  };

  // ... return the provider
};

In App.tsx, you need to pass the handexTermRef to the CommandProvider:

// App.tsx

const App = () => {
  // ... other state and logic

  return (
    <CommandProvider handTermRef={handexTermRef}>
      {/* ... components */}
    </CommandProvider>
  );
};

In the command definition, you would then be able to use the HandTerm instance:

// ICommand definition

interface ICommand {
  // ... other properties
  execute: (
    args?: string[],
    switches?: Record<string, boolean | string>,
    handTerm?: HandTerm
  ) => string;
}

Now, when you define your commands, you can assume they will receive the HandTerm context as an argument:

// ListPhrasesCommand.ts

export const ListPhrasesCommand: ICommand = {
  // ... other properties
  execute: (_args?: string[], _switches?: Record<string, boolean | string>, handTerm?: HandTerm) => {
    if (!handTerm) {
      return 'HandTerm context is not available';
    }

    // Access HandTerm methods or state
    // ...

    const phrases = Phrases.getPhrases().join('\n');
    return phrases;
  }
};

This method allows you to execute commands with access to HandTerm without manually passing the context each time. However, it does require that you structure your commands to expect this HandTerm instance as a parameter. This is one way to maintain access to the context without prop drilling while still leveraging the power of React's Context API.


User at 9:58 am, Jun 14, 2024

ok, that is working.

Now I have a question about the structure of my application.

The HandTerm is a terminal UI that allows users to go into contects, such as the game context or the phrase context.

I want any commands that I run to be able to append output to the HandTerm.outputRef, but I want to do it in the most modern React best practices.

So, the HandTerm will allow many commands to run, but shoudl also provide a way to write to the output, or the HandTerm should handle writing to the output, but also a way to clear the output.

The output is a React component that looks like this:

// src/components/Output.tsx

import React, { TouchEventHandler } from "react";
import { TerminalCssClasses } from "../types/TerminalTypes";

export interface OutputProps {
  elements: React.ReactNode[];
  onTouchStart: TouchEventHandler<HTMLDivElement>;
  onTouchEnd: TouchEventHandler<HTMLDivElement>;
}

export const Output = React.forwardRef<HTMLDivElement, OutputProps>(({ elements, onTouchStart, onTouchEnd }, ref) => {
  return (
    <div
      id={TerminalCssClasses.Output}
      className={TerminalCssClasses.Output}
      onTouchStart={onTouchStart}
      onTouchEnd={onTouchEnd}
      ref={ref}
    >
      {elements.map((element, index) => {
        if (typeof element === 'string') {
          return <div key={index} dangerouslySetInnerHTML={{ __html: element }} />;
        } else {
          return <div key={index}>{element}</div>;
        }
      })}
    </div>
  );
});

I want to be able to add commands to the HandTerm like adding plugins. I already have a commandRegistry which handles that.

The code is still in the early phases of creating a TUI CLI, but it is somewhat working already. I have an if ladder in HandTerm that handles several commands, such as clear, but I wanted to get a command abstraction in place first before I add many more commands.

Please advise on this structure and how it conforms to best practices, and also help me implement wrting to the output, and clearing the output in the CommandContext structure.


Codeium at 9:58 am, Jun 14, 2024

To align with modern React best practices and to provide a clean structure for your application, you would want to follow a few key principles:

  1. Keep the state and logic for updating the state close to where it's rendered. This often means lifting state up to the nearest common ancestor of components that need it.

  2. Use context to share global data or functions across the component tree without prop drilling.

  3. Encapsulate reusable logic in hooks or utility functions.

Given these principles, you can structure your HandTerm component and CommandContext to allow commands to update the output cleanly and efficiently.

HandTerm State Management

Your HandTerm component should manage the state related to the output, including adding new elements to the output and clearing it. It would expose functions for these actions, which commands can call.

CommandContext and CommandProvider

The CommandContext should provide access to functions that can manipulate the output. Your CommandProvider would then use a ref to HandTerm to call the appropriate functions on it.

Here's an example of how you might structure this:

// CommandContext.tsx

export interface ICommandContext {
  // ... other context properties
  appendToOutput: (element: React.ReactNode) => void;
  clearOutput: () => void;
}

In HandTerm.tsx, you would have functions to update the output:

class HandTerm extends React.Component<IHandexTermProps, IHandTermState> {
  // ... other component logic

  // Function to append to the output
  appendToOutput = (element: React.ReactNode) => {
    this.setState(prevState => ({
      outputElements: [...prevState.outputElements, element]
    }));
  };

  // Function to clear the output
  clearOutput = () => {
    this.setState({ outputElements: [] });
  };

  // ... rest of the component
}

Your CommandProvider would look something like this:

export const CommandProvider: React.FC<{ children?: React.ReactNode, handTermRef: React.RefObject<HandTerm> }> = ({ children, handTermRef }) => {
  // ... other state and logic for executing commands

  const appendToOutput = (element: React.ReactNode) => {
    const handTerm = handTermRef.current;
    if (handTerm) {
      handTerm.appendToOutput(element);
    }
  };

  const clearOutput = () => {
    const handTerm = handTermRef.current;
    if (handTerm) {
      handTerm.clearOutput();
    }
  };

  const contextValue = useMemo<ICommandContext>(() => ({
    // ... other context values
    appendToOutput,
    clearOutput
  }), [appendToOutput, clearOutput]);

  return (
    <CommandContext.Provider value={contextValue}>
      {children}
    </CommandContext.Provider>
  );
};

Using the Context in Commands

Commands would access the context to manipulate the output:

// In a command file

const ClearCommand: ICommand = {
  // ... command properties
  execute: (_args, _switches, handTermContext) => {
    // Use the context to clear the output
    handTermContext.clearOutput();
    return 'Output cleared';
  }
};

This structure keeps your output state management within HandTerm, leverages context to avoid prop drilling, and provides a clear way for commands to manipulate the output. It's scalable because adding new commands doesn't require changes to HandTerm; instead, you register them with the commandRegistry, and they use the provided context to interact with the output.

⚠️ **GitHub.com Fallback** ⚠️