📜 ⬆️ ⬇️

How we communicate with React components using TypeScript decorators

When developing applications for React, it is rather inconvenient to create components that are independent of each other, since The standard way to exchange data between them is " Lifting State Up ". This approach gradually pollutes the intermediate components with unnecessary properties, making them inconvenient for reuse.

image

The most popular means of solving this problem (and some others) are such libraries as Redux and Mobx, which allow you to store data in a separate place and transfer it to components directly. In this article I want to demonstrate our approach to solving this issue.

A separate page in the Docsvision EDMS is collected in a special WYSIWYG editor from a variety of React components placed at the required positions:

image

Partners can write their own JavaScript scripts that interact with components through their API (read / write properties, call methods), and can render the same components through the usual JSX syntax (for example, inside some modal windows or their own components).
')
Thus, our components should offer three ways to interact:

  1. Getting the parameters from the server configured in the WYSIWYG editor.
  2. Interaction using JavaScript scripts.
  3. Interaction through JSX.

To support all these modes of interaction, we developed a parameter system that somewhat improves the standard property mechanism. It looks like this:

//         . let textBox = layout.controls.textBox; //       . textBox.params.value = " "; //       . textBox.params.dataChanged = (sender, data)=> sender.params.value = data.newValue.toUpperCase(); 

And this is how the class itself works with the parameters of the TextBox component:

 class TextBoxParams { /** . */ @rw value?: string = ''; /**    . */ @r canEdit?: boolean; /** ,    . */ @apiEvent dataChanged?: BasicApiEvent<string>; } 

As we can see, besides the usual listing of properties, as in the standard property mechanism, there are also decorators @r, @rw and @apiEvent. With their help, we create more flexible behavior for our properties.

And since the same class is also used as an interface for React-properties, we can interact with the component in the same way both with external scripts and through JSX.


The most commonly used decorators for properties are:

The name of the decoratorDescription
  @r 
The property is read-only, not allowing to change it.
  @rw 
The property is available for both reading and writing.
  @apiEvent 
Indicates that we must treat the property value as an event handler for component events with the same name. Also, when working with such properties, we implement event-specific logic (for example, automatic unsubscribe of the previous handler when setting a new property value).
  @handler (paramName) 
Unlike those listed above, this decorator is hung not on a property, but on any getter or setter inside a component. This allows you to add your own logic when writing or reading the value of a property. For example, trimming spaces from the beginning and end of a value:

 class TextBoxParams { /** . */ @rw value?: string = ''; } class TextBox extends BaseControl<TextBoxProps, TextBoxState> { ... @handler('value') private get value(): string { return this.state.value.trim(); } ... } 


At the same time, the decorators themselves usually do not contain any business logic, but merely retain the information that the decorator was used for. This is done with the help of the reflect-metadata library and is convenient in that it becomes possible to store logic in another place, flexibly combining several associated metadata. Consider using this library in a simplified example with the @r decorator:

 //        @r. const READONLY_DECORATOR_METADATA_KEY = "CONTOL_PUBLIC_API_READONLY"; //   @r,       . export function r(target: Object, propertyKey: string | symbol) { Reflect.defineMetadata(READONLY_DECORATOR_METADATA_KEY, true, target, propertyKey); } //        @r  . export function isReadonly(target: Object, propertyKey: string): boolean { return Reflect.getMetadata(READONLY_DECORATOR_METADATA_KEY, target, propertyKey); } 

After applying this decorator on any property of the object, metadata with the name “CONTOL_PUBLIC_API_READONLY” and the value true will automatically become attached to this property.

Using such metadata, we can dynamically set the desired behavior to our parameters (access modifiers, work with events from the table above, etc.). An example of the simplest implementation is given below the spoiler.

Sample code with implementation
 class TextAreaParams { @r value: string = ''; } /** .  1 . */ interface ITextAreaState extends TextAreaParams { } class TextArea extends React.Component<TextAreaParams, ITextAreaState> { /** .  2 . */ params: TextAreaParams = {} as TextAreaParams; constructor(props: ITextAreaProps) { super(props); /** .  3 . */ this.state = new TextAreaParams() as ITextAreaState; /** .  4 . */ for (let propName in this.state) { let descriptor = { get: () => this.getParamValue(propName), set: (value: any) => this. (propName, value), configurable: true, enumerable: true } as PropertyDescriptor; Object.defineProperty(this.params, propName, descriptor); } /** .  5 . */ for (let propName in this.props) { this.setParamValue(propName, this.props[propName], true); } } /** .  6 . */ componentWillReceiveProps(nextProps: ITextAreaProps) { for (let propName in this.props) { if (this.props[propName] != nextProps[propName]) { this.setParamValue(propName, this.props[propName]); } } } /** .  7 . */ getParamValue(paramName: string) { return this.state[paramName]; } /** .  8 . */ setParamValue(paramName: string, value: any, initial: boolean) { const readOnly = isReadonly(this.state, paramName); if (!readOnly || initial) { this.state[paramName] = val; this.forceUpdate(); } else { if (this.props[paramName] != value) { console.warn(" " + paramName + "    ."); } } } } 

  1. The interface for the state component is inherited from the Params class, ensuring consistency of data within them. In addition, the same class is used as an interface for properties.
  2. Create an empty object for future work with params. Properties in it will be filled later.
  3. Create a state component, which is an instance of our class for params.
  4. Fill in the params properties. As you can see, the params object itself does not store any data, but uses the getParamValue and setParamValue methods as a getter and setter.
  5. Synchronize the original props values ​​with params.
  6. When new props values ​​are received, we also synchronize them with params. Based on this and the previous paragraph, it is clear that React-properties transfer their values ​​through parameters to the state component, which allows using decorators for them as well.
  7. The value for the parameters is simply obtained from the property with the same name from the state, since He is for us "the single source of truth."
  8. When a new parameter value is set, it is checked whether our @r decorator is applied to a property using the isReadonly helper created above. If the property is read-only, a warning about this is displayed in the browser console and the value does not change, otherwise new data is simply written to the state.


Thus, we obtained a universal API for accessing a component through the React properties when used inside another component, and when we receive a reference to the component and further work with it as an object. And with the help of decorators work with them is simple and clear.

I hope the demonstration of our approach will allow someone to simplify their API for working with components, and for a more detailed acquaintance with the decorators in TypeScript I recommend the article of my colleague The dark side of TypeScript is @ decorators using examples .

Source: https://habr.com/ru/post/350646/


All Articles