Skip to content

Commit

Permalink
Proposal: Use React context instead of prop drilling for accessing sh…
Browse files Browse the repository at this point in the history
…ared state

- Refactor  app component  logic into a context provider 
- Wrap  the root component into the newly created `MemoryAppProvider`.
  This way child components can access the central state via context API and props drilling is no longer required.
- Refactor child components to access the central state info via `context` instead of props.
  • Loading branch information
tortmayr committed Feb 27, 2024
1 parent d1400a1 commit cbd9921
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 291 deletions.
199 changes: 199 additions & 0 deletions src/webview/components/memory-app-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/********************************************************************************
* Copyright (C) 2022 Ericsson, Arm and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { DebugProtocol } from '@vscode/debugprotocol';
import React from 'react';
import { HOST_EXTENSION } from 'vscode-messenger-common';
import { logMessageType, readMemoryType, readyType, resetMemoryViewSettingsType, setMemoryViewSettingsType, setOptionsType, setTitleType } from '../../common/messaging';
import { AddressColumn } from '../columns/address-column';
import { AsciiColumn } from '../columns/ascii-column';
import { ColumnStatus, columnContributionService } from '../columns/column-contribution-service';
import { DataColumn } from '../columns/data-column';
import { decorationService } from '../decorations/decoration-service';
import { Decoration, Memory, MemoryDisplayConfiguration, MemoryState } from '../utils/view-types';
import { variableDecorator } from '../variables/variable-decorations';
import { messenger } from '../view-messenger';

export interface MemoryAppState extends MemoryState, MemoryDisplayConfiguration {
title: string;
decorations: Decoration[];
columns: ColumnStatus[];
offset: number;
}

const MEMORY_DISPLAY_CONFIGURATION_DEFAULTS: MemoryDisplayConfiguration = {
bytesPerWord: 1,
wordsPerGroup: 1,
groupsPerRow: 4,
scrollingBehavior: 'Paginate',
addressRadix: 16,
showRadixPrefix: true,
};

const MEMORY_APP_STATE_DEFAULTS: MemoryAppState = {
title: 'Memory',
memory: undefined,
memoryReference: '',
offset: 0,
count: 256,
decorations: [],
columns: columnContributionService.getColumns(),
isMemoryFetching: false,
...MEMORY_DISPLAY_CONFIGURATION_DEFAULTS
};

const MEMORY_APP_CONTEXT_DEFAULTS: MemoryAppContext = {
...MEMORY_APP_STATE_DEFAULTS,
updateMemoryState: () => { },
updateMemoryDisplayConfiguration: () => { },
resetMemoryDisplayConfiguration: () => { },
updateTitle: () => { },
refreshMemory: () => { },
fetchMemory: async () => { },
toggleColumn: () => { }
};

export const MemoryAppContext = React.createContext<MemoryAppContext>(MEMORY_APP_CONTEXT_DEFAULTS);

export interface MemoryAppContext extends MemoryAppState {
updateMemoryState: (newState: Partial<MemoryAppState>) => void;
updateMemoryDisplayConfiguration: (newState: Partial<MemoryDisplayConfiguration>) => void;
resetMemoryDisplayConfiguration: () => void;
updateTitle: (title: string) => void;
refreshMemory: () => void;
fetchMemory: (partialOptions?: Partial<DebugProtocol.ReadMemoryArguments>) => Promise<void>;
toggleColumn: (id: string, active: boolean) => void;
}

interface MemoryAppProviderProps {
children: React.ReactNode;
}

export class MemoryAppProvider extends React.Component<MemoryAppProviderProps, MemoryAppState> {

public constructor(props: MemoryAppProviderProps) {
super(props);
columnContributionService.register(new AddressColumn(), false);
columnContributionService.register(new DataColumn(), false);
columnContributionService.register(variableDecorator);
columnContributionService.register(new AsciiColumn());
decorationService.register(variableDecorator);
this.state = {
title: 'Memory',
memory: undefined,
memoryReference: '',
offset: 0,
count: 256,
decorations: [],
columns: columnContributionService.getColumns(),
isMemoryFetching: false,
...MEMORY_DISPLAY_CONFIGURATION_DEFAULTS
};
}

public componentDidMount(): void {
messenger.onRequest(setOptionsType, options => this.setOptions(options));
messenger.onNotification(setMemoryViewSettingsType, config => {
for (const column of columnContributionService.getColumns()) {
const id = column.contribution.id;
const configurable = column.configurable;
this.toggleColumn(id, !configurable || !!config.visibleColumns?.includes(id));
}
this.setState(prevState => ({ ...prevState, ...config, title: config.title ?? prevState.title, }));
});
messenger.sendNotification(readyType, HOST_EXTENSION, undefined);
}

public render(): React.ReactNode {
const contextValue: MemoryAppContext = {
...this.state,
updateMemoryState: this.updateMemoryState,
fetchMemory: this.fetchMemory,
refreshMemory: this.refreshMemory,
resetMemoryDisplayConfiguration: this.resetMemoryDisplayConfiguration,
toggleColumn: this.toggleColumn,
updateMemoryDisplayConfiguration: this.updateMemoryDisplayConfiguration,
updateTitle: this.updateTitle
};

return (
<MemoryAppContext.Provider value={contextValue}>
{this.props.children}
</MemoryAppContext.Provider>
);
}

protected updateMemoryState = (newState: Partial<MemoryState>) => this.setState(prevState => ({ ...prevState, ...newState }));
protected updateMemoryDisplayConfiguration = (newState: Partial<MemoryDisplayConfiguration>) => this.setState(prevState => ({ ...prevState, ...newState }));
protected resetMemoryDisplayConfiguration = () => messenger.sendNotification(resetMemoryViewSettingsType, HOST_EXTENSION, undefined);
protected updateTitle = (title: string) => {
this.setState({ title });
messenger.sendNotification(setTitleType, HOST_EXTENSION, title);
};

protected async setOptions(options?: Partial<DebugProtocol.ReadMemoryArguments>): Promise<void> {
messenger.sendRequest(logMessageType, HOST_EXTENSION, `Setting options: ${JSON.stringify(options)}`);
this.setState(prevState => ({ ...prevState, ...options }));
return this.fetchMemory(options);
}

protected refreshMemory = () => { this.fetchMemory(); };

protected fetchMemory = async (partialOptions?: Partial<DebugProtocol.ReadMemoryArguments>): Promise<void> => this.doFetchMemory(partialOptions);
protected async doFetchMemory(partialOptions?: Partial<DebugProtocol.ReadMemoryArguments>): Promise<void> {
this.setState(prev => ({ ...prev, isMemoryFetching: true }));
const completeOptions = {
memoryReference: partialOptions?.memoryReference || this.state.memoryReference,
offset: partialOptions?.offset ?? this.state.offset,
count: partialOptions?.count ?? this.state.count
};

try {
const response = await messenger.sendRequest(readMemoryType, HOST_EXTENSION, completeOptions);
await Promise.all(Array.from(
new Set(columnContributionService.getUpdateExecutors().concat(decorationService.getUpdateExecutors())),
executor => executor.fetchData(completeOptions)
));

this.setState({
decorations: decorationService.decorations,
memory: this.convertMemory(response),
memoryReference: completeOptions.memoryReference,
offset: completeOptions.offset,
count: completeOptions.count,
isMemoryFetching: false
});

messenger.sendRequest(setOptionsType, HOST_EXTENSION, completeOptions);
} finally {
this.setState(prev => ({ ...prev, isMemoryFetching: false }));
}

}

protected convertMemory(result: DebugProtocol.ReadMemoryResponse['body']): Memory {
if (!result?.data) { throw new Error('No memory provided!'); }
const address = BigInt(result.address);
const bytes = Uint8Array.from(Buffer.from(result.data, 'base64'));
return { bytes, address };
}

protected toggleColumn = (id: string, active: boolean): void => { this.doToggleColumn(id, active); };
protected async doToggleColumn(id: string, isVisible: boolean): Promise<void> {
const columns = isVisible ? await columnContributionService.show(id, this.state) : columnContributionService.hide(id);
this.setState(prevState => ({ ...prevState, columns }));
}
}
67 changes: 36 additions & 31 deletions src/webview/components/memory-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@
********************************************************************************/

import { DebugProtocol } from '@vscode/debugprotocol';
import isDeepEqual from 'fast-deep-equal';
import memoize from 'memoize-one';
import { Column } from 'primereact/column';
import { DataTable, DataTableCellSelection, DataTableProps, DataTableRowData, DataTableSelectionCellChangeEvent } from 'primereact/datatable';
import { ProgressSpinner } from 'primereact/progressspinner';
import { classNames } from 'primereact/utils';
import React from 'react';
import { TableRenderOptions } from '../columns/column-contribution-service';
import { Decoration, Memory, MemoryDisplayConfiguration, ScrollingBehavior, isTrigger } from '../utils/view-types';
import isDeepEqual from 'fast-deep-equal';
import { AddressColumn } from '../columns/address-column';
import { classNames } from 'primereact/utils';
import { ColumnStatus, TableRenderOptions } from '../columns/column-contribution-service';
import { Endianness, Memory, MemoryDisplayConfiguration, ScrollingBehavior, isTrigger } from '../utils/view-types';
import { MemoryAppContext } from './memory-app-provider';

export interface MoreMemorySelectProps {
count: number;
Expand Down Expand Up @@ -93,13 +94,9 @@ export const MoreMemorySelect: React.FC<MoreMemorySelectProps> = ({ count, offse
);
};

interface MemoryTableProps extends TableRenderOptions, MemoryDisplayConfiguration {
memory?: Memory;
decorations: Decoration[];
offset: number;
count: number;
fetchMemory(partialOptions?: Partial<DebugProtocol.ReadMemoryArguments>): Promise<void>;
isMemoryFetching: boolean;
interface MemoryTableProps {
endianness: Endianness;
columnOptions: ColumnStatus[];
}

interface MemoryRowListOptions {
Expand All @@ -118,10 +115,10 @@ interface MemoryTableState {
selection: DataTableCellSelection<MemoryRowData[]> | null;
}

type MemorySizeOptions = Pick<MemoryTableProps, 'bytesPerWord' | 'wordsPerGroup' | 'groupsPerRow'>;
type MemorySizeOptions = Pick<MemoryDisplayConfiguration, 'bytesPerWord' | 'wordsPerGroup' | 'groupsPerRow'>;
namespace MemorySizeOptions {
export function create(props: MemoryTableProps): MemorySizeOptions {
const { groupsPerRow, bytesPerWord, wordsPerGroup }: MemorySizeOptions = props;
export function create(context: MemoryAppContext): MemorySizeOptions {
const { groupsPerRow, bytesPerWord, wordsPerGroup }: MemorySizeOptions = context;
return {
bytesPerWord,
groupsPerRow,
Expand All @@ -131,11 +128,15 @@ namespace MemorySizeOptions {
}

export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTableState> {
static contextType = MemoryAppContext;
declare context: MemoryAppContext;

protected prevContext?: MemoryAppContext;

protected datatableRef = React.createRef<DataTable<MemoryRowData[]>>();

protected get isShowMoreEnabled(): boolean {
return !!this.props.memory?.bytes.length;
return !!this.context.memory?.bytes.length;
}

constructor(props: MemoryTableProps) {
Expand All @@ -151,24 +152,28 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
};
}

componentDidUpdate(prevProps: Readonly<MemoryTableProps>): void {
const hasMemoryChanged = prevProps.memory?.address !== this.props.memory?.address || prevProps.offset !== this.props.offset || prevProps.count !== this.props.count;
const hasOptionsChanged = prevProps.wordsPerGroup !== this.props.wordsPerGroup || prevProps.groupsPerRow !== this.props.groupsPerRow;

// Reset selection
const selection = this.state.selection;
if (selection && (hasMemoryChanged || hasOptionsChanged)) {
// eslint-disable-next-line no-null/no-null
this.setState(prev => ({ ...prev, selection: null }));
componentDidUpdate(): void {
if (this.prevContext && this.prevContext !== this.context) {
const hasMemoryChanged = this.prevContext.memory?.address !== this.context.memory?.address
|| this.prevContext.offset !== this.context.offset || this.prevContext.count !== this.context.count;
const hasOptionsChanged = this.prevContext.wordsPerGroup !== this.context.wordsPerGroup || this.prevContext.groupsPerRow !== this.context.groupsPerRow;

// Reset selection
const selection = this.state.selection;
if (selection && (hasMemoryChanged || hasOptionsChanged)) {
// eslint-disable-next-line no-null/no-null
this.setState(prev => ({ ...prev, selection: null }));
}
}
this.prevContext = this.context;
}

public render(): React.ReactNode {
const memory = this.props.memory;
const memory = this.context.memory;
let rows: MemoryRowData[] = [];

if (memory) {
const memorySizeOptions = MemorySizeOptions.create(this.props);
const memorySizeOptions = MemorySizeOptions.create(this.context);
const options = this.createMemoryRowListOptions(memory, memorySizeOptions);
rows = this.createTableRows(memory, options);
}
Expand All @@ -185,15 +190,15 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
>
{this.props.columnOptions.map(({ contribution }) => {
const fit = contribution.id === AddressColumn.ID;

const renderOptions: TableRenderOptions = { columnOptions: this.props.columnOptions, endianness: this.props.endianness, ...this.context };
return <Column
key={contribution.id}
field={contribution.id}
header={contribution.label}
className={classNames({ fit })}
headerClassName={classNames({ fit })}
style={{ width: fit ? undefined : `${columnWidth}%` }}
body={(row?: MemoryRowData) => row && contribution.render(row, this.props.memory!, this.props)}>
body={(row?: MemoryRowData) => row && contribution.render(row, this.context.memory!, renderOptions)}>
{contribution.label}
</Column>;
})}
Expand Down Expand Up @@ -228,7 +233,7 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
};

protected renderHeader(): React.ReactNode | undefined {
const { offset, count, fetchMemory, scrollingBehavior } = this.props;
const { offset, count, fetchMemory, scrollingBehavior } = this.context;

let memorySelect: React.ReactNode | undefined;
let loading: React.ReactNode | undefined;
Expand All @@ -246,7 +251,7 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
</div>;
}

if (this.props.isMemoryFetching) {
if (this.context.isMemoryFetching) {
loading = <div className='absolute right-0 flex align-items-center'>
<ProgressSpinner style={{ width: '16px', height: '16px' }} className='mr-2' />
<span>Loading</span>
Expand All @@ -262,7 +267,7 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
}

protected renderFooter(): React.ReactNode | undefined {
const { offset, count, fetchMemory, scrollingBehavior } = this.props;
const { offset, count, fetchMemory, scrollingBehavior } = this.context;

let memorySelect: React.ReactNode | undefined;

Expand Down
Loading

0 comments on commit cbd9921

Please sign in to comment.