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
ICommandContext
had a self-reference issue due to theexecuteCommand
method taking a context parameter. - Suggestions were provided to refactor
ICommandContext
to remove the self-reference, allowing access to context usinguseContext
orcontextType
. - A discussion on accessing
HandTerm
context within commands by passing the context as an argument or binding it when calling theexecute
method. - Refactoring suggestions were given to structure commands as methods of
HandTerm
or 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
HandTerm
andCommandContext
. - The proposed structure included managing output state and functions within
HandTerm
and usingCommandContext
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
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
execute
fromHandTerm
. - 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:
-
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.