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:
- The initial implementation of
ICommandContexthad a self-reference issue due to theexecuteCommandmethod taking a context parameter. - Suggestions were provided to refactor
ICommandContextto remove the self-reference, allowing access to context usinguseContextorcontextType. - A discussion on accessing
HandTermcontext within commands by passing the context as an argument or binding it when calling theexecutemethod. - Refactoring suggestions were given to structure commands as methods of
HandTermor using a higher-order component to inject context. - The importance of avoiding prop drilling by utilizing React's Context API was emphasized.
- Suggestions were provided on how to structure the application for adding commands as plugins and handling output manipulation within the
HandTermandCommandContext. - The proposed structure included managing output state and functions within
HandTermand usingCommandContextto 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
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:
- Bind the context when you call
executefromHandTerm. - Pass the necessary data as arguments to the
executefunction.
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:
-
Make commands methods of
HandTerm. This way, they can naturally accessthis.context. -
Use a higher-order component or custom hook to inject context into your commands.
-
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:
-
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.
-
Use context to share global data or functions across the component tree without prop drilling.
-
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.
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.
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>
);
};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.