I have a React component that is a report with many buttons. You can open some subreports on table row click or by button click. I keep in state boolean values for each report. Basically my State interface for types looks like this:
interface State {
isSubreportVisible: boolean;
isLogsVisible: boolean;
isSelectStatusVisible: boolean;
isMoneyVisible: boolean;
isTargetStoreWarningVisible: boolean;
isMultiVisible: boolean;
isCancelVisible: boolean;
isPrintVisible: boolean;
}
Then my initial state is like this:
public state: State = {
isSubreportVisible: false,
isLogsVisible: false,
isSelectStatusVisible: false,
isMoneyVisible: false,
isMultiVisible: false,
isTargetStoreWarningVisible: false,
isCancelVisible: false,
isPrintVisible: false,
};
And inside component I have:
{isSubreportVisible && (
<Subreport
...
...
...
/>
)}
File of the component is really long - almost 600 lines of code. Is there anything could be done here? All subreports in modals are connected to this component?
Is it a good pattern to create a file called for example "types.tsx" and move all interfaces there (State, StateToProps, Props etc.)? Any ideas? Maybe it could be better to have just one string value in state - currentModalVisible and to keep there its names from enum?
Maybe it could be better to have just one string value in state - currentModalVisible and to keep there its names from enum?
This depends -- is only one modal visible at a time? If so then I would store a single value which is either null/undefined or the name of the visible modal. If the modals toggle independently then you will want to store a boolean for each one.
Either way, I think a good goal here is to have generalized functions that take a string key for the modal as an argument. You want to be able to call things like isModalVisible('print'), toggleModalVisible('print'), setModalVisible('print', false), etc.
I would define a type for the props that are available to the modals/subreports. You want to require that the components cannot require any additional props. That way you can call a component by its variable and pass it all the available props some will be ignored).
For maximum flexibility, you could even pass the available modals as a prop.
interface ModalProps {
someKey: string;
}
interface FlexibleSettings {
// what to display in your tab/toggle
// optional because can default to a cased version of the `key`
buttonText?: React.ReactNode;
// the component to render when selected
modal: React.ComponentType<ModalProps>;
}
interface VeryFlexibleComponentProps<Keys extends string> {
// pass the modals as an object keyed by the modal key
modals: Record<Keys, FlexibleSettings>;
// can start with one visible
initialVisible?: Keys;
// need to access the ModalProps somewhere
// maybe some come from this component and can be omitted
modalProps: ModalProps;
}
const VeryFlexibleComponent = <Keys extends string>({
modals,
initialVisible,
modalProps
}: VeryFlexibleComponentProps<Keys>) => {
const [visible, setVisible] = React.useState(initialVisible); // type Keys | undefined
// need an uppercase variable to call a component with JSX
const CurrentModal = visible ? modals[visible].modal : undefined;
return (
<div>
<h3>Select Modal</h3>
{(Object.keys(modals) as Keys[]).map((key) => (
<div key={key}>
<input
type="radio"
name="visible"
value={key}
id={key}
checked={visible === key}
onChange={(e) => {
if (e.target.checked) setVisible(key);
}}
/>
<label htmlFor={key}>
{modals[key].buttonText ?? _startCase(key)}
</label>
</div>
))}
{CurrentModal !== undefined && (
<div>
<h3>Viewing Modal</h3>
<CurrentModal {...modalProps} />
</div>
)}
</div>
);
};
Code Sandbox
Related
I created a stencil component () that uses another stencil component () in the rendered template.
One of the props (key: string) passed from smg-compound-filter to smg-filter is undefined, whereas the other props are well defined. I made sure that {filter.key} is defined in the template of smg-compound-filter, and i even tried to pass a literal string to smg-filter, but it's undefined in this component. I think i am missing something. If someone could give me an insight, it will be bery helpful.
smg-compound-filter.ts (render function)
render() {
return (
<div class="smg-filter-container">
<div class={`filters`}>
this.filters.map(filter => {
return (
<div class='filter'>
<smg-filter
key={filter.key} // even "str" doesn't work
label={filter.label}
target={filter.target}
options={filter.options}
customClasses='secondary'
onFilterChanged={(event) => {this.toggleOption(event)}}
>
</smg-filter>
</div>
);
})
</div>
</div>
);
}
smg-filter.ts
export class SmgFilter {
#State() filter: Filter;
#State() showOptions: boolean;
/** custom classes to adapt style */
#Prop() customClasses: string;
/** smartsheet column linked to the filter */
#Prop() key: string;
/** label of the filter */
#Prop() label: string;
/** options */
#Prop() options: FilterOption[];
/** type of products to be filtered: 'master' or 'product' */
#Prop() target: FilterTarget;
#Event() filterChanged: EventEmitter<Filter>;
componentWillLoad() {
console.log(this.key); // undefined
this.showOptions = !isSecondary ? true : false;
this.filter = {
key: this.key,
target: this.target,
label: this.label,
options: this.options,
};
}
That's because key is a reserved keyword since Stencil uses it to improve loop rerender performance.
I'm not 100% sure if it can be changed or disabled but I think you'll have to use another property name.
The #Prop() name "key" is a reserved public name. Please rename the "key" prop so it does not conflict with
an existing standardized prototype member. Reusing prop names that are already defined on the element's
prototype may cause unexpected runtime errors or user-interface issues on various browsers, so it's best to
avoid them entirely.
I'm having trouble understanding Typescript. I want to define a <Component/> with one required prop requiredProp, and a condition prop extend which if true, allows to use extendedProp.
e.g.
<Component requiredProp={''} /> // OK
<Component requiredProp={''} extend={true} requiredProp={Function} /> // OK
<Component requiredProp={''} requiredProp={Function} /> // ERROR
<Component requiredProp={''} extend={true} /> // ERROR
My code:
// types
interface RequiredProps {
requiredProp: 'string';
}
type ExtendedProps =
| {
extend?: false;
extendedProp?: never;
}
| {
extend: true;
extendedProp: Function;
};
type ComponentProps = RequiredProps & ExtendedProps;
// component
const Component: React.FC<ComponentProps> = ({requiredProp, extend, extendedProp}) => {
if (extend) {
extendedProp(); // ERROR: Cannot invoke an object which is possibly 'undefined'.
}
}
If I am checking extend to be true, shouldn't TS automatically know from ExtendedProps that extendedProp is also defined?
Compiler is only satisfied if I am explicitly checking extendedProp to be defined:
if (extendedProp) {
extendedProp(); // no error
}
If you do this using the entire props object, it will work. checking for props.extend will narrow down the type on props, and thus props.extendedProp will be allowed:
const Component: React.FC<ComponentProps> = (props) => {
if (props.extend) {
props.extendedProp();
}
}
But if you've already split them up into separate variables, then this can't happen. props isn't involved in your if statement at all, so the type on props can't be narrowed. There are just two unrelated local variables, and checking the type on one doesn't do anything to the type on the other.
You and i can recognize that the two variables are related to eachother, but typescript doesn't have the ability to back track to find the origin of the type, and deduce what that means for other types. It may seem simple in this case, but keep in mind that there are a huge variety of lines of code that could be written that would break the linkage, such as:
const Component: React.FC<ComponentProps> = ({requiredProp, extend, extendedProp}) => {
extend = Math.random() > 0.5
if (extend) {
extendedProp();
}
}
Figuring out the implications of those kinds of things is impractical (or maybe impossible);
Playground link
I have a class where I declare:
constructor() {
super();
this.state = {
checked: false,
house: [],
selectedHouse: null
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(checked) {
this.setState({ checked });
}
render() {
return (
<React.Fragment>
<TSwitch handleChange={this.handleChange.bind(this)} house={this.state.house} houseClicked={this.h}></TSwitch>
</React.Fragment>
);
}
I then want to set state.checked from a child component:
function TSwitch(props) {
const handleChange = (house) => (evt) => {
props.handleChange(house);
};
return (
<div>
{props.house.map((house) => {
return (
<label>
<span>Switch with default style</span>
<Switch onChange={handleChange} checked={this.state.checked} />
</label>
);
})}
</div>
);
}
I am able to call handleChange but I want to be able to change the value of state.checked from the <TSwitch/> component.
This is what your parent component should be like:
constructor() {
super();
this.state = {
checked: false,
house: [],
selectedHouse: null
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(checked) {
this.setState({ checked });
}
render() {
return (
<React.Fragment>
<TSwitch handleChange={this.handleChange} isChecked={this.state.checked} house={this.state.house}></TSwitch>
</React.Fragment>
);
}
This is what your child component should look like:
function TSwitch(props) {
return (
<div>
{props.house.map((house) => {
return (
<label>
<span>Switch with default style</span>
<Switch onChange={x => props.handleChange(x)} checked={props.isChecked} />
</label>
);
})}
</div>
);
}
NOTE: You are using a Switch component, I'm not sure if the variable x will be a boolean or an object, but most probably it should be a boolean: true or false. If this doesn't work, log the value of x & see if its an object, and pass the boolean in props.handleChange. Although I still think this won't be needed. Good luck!
1.
Let's start with your direct question
I want to be able to change the value of state.checked from the <TSwitch/> component
1.1 You've correctly passed your mutator function handleChange from the Parent class to TSwitch but your abstraction function handleChange inside that child, that you've duplicated, is unnecessary and should be removed completely.
1.2 Next, going back to the class' handleChange function, you need to modify the handleChange function definition in the parent component, by fixing the argument you passed it -- which will be the event object, passed implicitly since you registered it as a callback to onChange={handleChange} inside Tswitch. At invocation time, it will be called, and the evt argument that's given to onChange from React, will be passed into handleChange. But, you don't need it. It carries no information of necessity to you. So I would ignore it entirely.
// # parent component
handleChange(evt) {
// NOTE: i'm going to ignore the evt, since I don't need it.
// NOTE: i'm going to use optional callback given by setState, to access prevState, and toggle the checked state boolean value.
this.setState((prevState) => ({ checked: !prevState.checked }));
}
2.
Now let's clean up your code and talk about some best practices
2.1 You dont' need to be using React.Fragment here. Why? because Fragments were introduced in React 16 to provide a declarative API for handling lists of components. Otherwise, they're unecessary abstractions. Meaning: if you're not directly dealing with sibling components, then you don't need to reach for React.Fragment just go with a <div/> instead; would be more idiomatic.
2.2. If <TSwitch></TSwitch> isn't going to have a direct descendent, then you should change your usage syntax to <TSwitch/>.
2.3 If 2.2 didnt' get picked up by a linter, then I highly advised you install one.
2.4 You can continue using explicit bindings of your class handlers in your constructor if you'd like. It's a good first step in learning React, however, there's optimal ways to remove this boilerplate via Babel's transform properties plugins.
This will work:
handleChange(checked) {
this.setState({ checked:!checked });
}
I am building an application that gets a list of job descriptions from backend and takes user input to select one of them along with several parameters in order to make a second backend request. Since there will be a few more components involved, I have decided to use Redux for managing global state, and that appears to cause complications in my code.
My DropdownButton component uses handleJobSelect to modify local state and thereby show current selectedJobValue as the value of a FormControl component, and all is apparently well and good if my items are rendered directly (e.g. "Good item" in the code below). If I use a fetched list to map my items, however, selecting one of them will somehow cause the entire website to reload, losing all state changes and resetting both the selectedJobValue and a FormControl value property.
class Settings extends Component {
constructor() {
super();
this.state = {
descriptions: [],
selectedJobValue: 'None'
};
this.handleJobSelect = this.handleJobSelect.bind(this);
}
componentDidMount() {
this.props.fetchData('http://127.0.0.1:5000/api/1.0/descriptions');
}
handleJobSelect = evt => {
const someVal = evt;
console.log(someVal);
console.log(this.state);
this.setState({ selectedJobValue: someVal });
console.log(this.state);
};
render() {
const { selectedJobValue } = this.state;
return (
<InputGroup classname="mb-3">
<DropdownButton
as={InputGroup.Prepend}
variant="outline-primary"
title="Description"
id="input-group-dropdown-1"
onSelect={this.handleJobSelect}
>
{this.props.items.map(item => (
<Dropdown.Item href={item} key={item} eventkey={item}>
{item.toString()}
</Dropdown.Item>
))}
<Dropdown.Item href="#">Good item</Dropdown.Item>
</DropdownButton>
<FormControl
placeholder="Placeholder job description"
aria-label="Job description"
aria-describedby="basic-addon1"
value={selectedJobValue}
readOnly="True"
/>
<InputGroup.Append>
<Button variant="outline-primary">Submit</Button>
</InputGroup.Append>
</InputGroup>
const mapStateToProps = state => {
return {
items: state.items,
};
};
const mapDispatchToProps = dispatch => {
return {
fetchData: url => dispatch(itemsFetchData(url))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Settings);
I suspect that my attempt to use both local and global state is complicit in this issue, but this seems impossible to avoid given my plans for the application. Is there a way to resolve the problem without abandoning Redux?
After going through my code a few more times, I think that the problem is in my use of href and handleJobSelect() function. Since my description items represent relative file paths with slashes, they are read as valid hrefs that can call the page by themselves, thereby ruining everything. Is it possible to extract through my handleJobSelect() function different properties of the target dropdown item, for example, its text (that is, {item})?
i just integrated flow for the first time to check my javascript sources statically.
I am struggling with a error flow finds and i am not able to solve it on my own. Its about using es6 classes and inheritance. More specific i created some react Components and they should inherit some methods.
I have a Callout Component, that represents a callout message of unspecified severity. To make things a little more simple i thought about providing a ErrorMessage Component, that inherits the Callout Component. My classes Structure looks like:
React.Component
> AbstractComponent (here i add some project-wide helpers for i18n and so on
> Callout (this represents a pretty message on the screen)
> ErrorMessage (this represents an error)
Flow tells me:
Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/js/Components/Generic/ErrorMessage.js:14:43
statics of Callout [1] is not a polymorphic type.
11│ icon: string
12│ };
13│
[1] 14│ export default class ErrorMessage extends Callout<Props> {
15│
16│ static get defaultProps(): Props {
17│ return {
The part Callout<Props> gets highlighted
I already define the Props Type for the Callout class, so this might be the problem but i cant solve it on my own.
A similar error is thrown a few lines below, where i try to access a parent method by addressing super.content (content is a get-method of Callout).
Thanks in advance
UPDATE: Why do i want to use class inheritance?
The inheritance Callout > ErrorMessage just exists to reduce redundant code, but its not necessary, so lets ignore this and talk about a more common case:
I want to have a class AbstractComponent to make common things in my project easier.
Some examples:
Printing of translation strings: In order to make the component multilingual, i created a utility class to generate translation strings, inside a component it works like
function render() {
return (
<div>
{new Translation(
'namespace',
'key',
[some, args],
`${some} fallback message with optional ${args}`
).toString()}
</div>
)
}
In order to use this, every component in my stack ends up with the import statement on top
import Translation from "../Core/Translation"
or in the best case
import t from "../Core/Translation"
I use webpack to build a bundle and webpack seems to blow up the compiled javascript with every import statement you use. So i figured - to reduce coding effort and bundle size - i provide a intermediate component class, that adds some utility methods like:
class AbstractComponent extends React.Component {
constructor(props) {
super(props);
this.logger = props.logger || new Logger();
this.api: ApiInterface = props.api || new MockApi();
}
translate(namespace: string, key: string, args: ?[] = null, fallback: ?string): string {
return new Translation(namespace, key, args, fallback).toString();
}
svgSprite(id: string, className: string = "") {
return (
<SvgSprite id={id} className={className} />
)
}
}
I also added some other things to show you more reason for a intermediate Component class.
So, all of this works! But flow complains about missing return types and so on, thats good with me, for that purpose i want to use flow! The problem i cant solve is the inheritance itself... But for me it does make a lot of sense.
If you really want to deal with inheritance (which I don't have an issue with, I just feel like you will probably run into issues later), you can do something like the following:
class AbstractComponent<Props: {}, State: ?{} = null> extends React.Component<Props, State> {
api: ApiInterface
logger: typeof Logger
constructor(props) {
super(props);
this.logger = props.logger || new Logger();
this.api = props.api || new MockApi();
}
translate(namespace: string, key: string, args: ?string[] = null, fallback: ?string): string {
return new Translation(namespace, key, args, fallback).toString();
}
svgSprite(id: string, className: string = "") {
return (
<SvgSprite id={id} className={className} />
)
}
}
And use it like:
class Test extends AbstractComponent<{ some: string, args: string }> {
render() {
const { some, args } = this.props
return (
<div>
{this.translate(
'namespace',
'key',
[some, args],
`${some} fallback message with optional ${args}`
)}
</div>
)
}
}
Now, I will say that to some extent I understand where Facebook is coming from. Your component in this case is really already an abstract construct. And if you want this to be more flexible (let's say you have a stateless component that could benefit from having a logger and a translate function), you could do one of two things:
This is the defined type and translate function I'm using in both:
type CommonProps = {
logger?: Logger,
api?: ApiInterface,
translate?: (namespace: string, key: string, args: ?string[], fallback: ?string) => string
}
// This should look familiar
function translate(namespace: string, key: string, args: ?string[] = null, fallback: ?string): string {
return new Translation(namespace, key, args, fallback).toString();
}
Higher order component
function addCommonStuff({ logger = new Logger(), api = new MockApi(), translate = translate }: CommonProps) {
return <Props: {}>(
WrappedComponent: ComponentType<Props>
): ComponentType<
$Diff<Props, $NonMaybeType<CommonProps>>
> => (props: Props) => <WrappedComponent {...props} logger={logger} api={api} translate={translate} />
}
And used like:
class Test extends React.Component<{}> {}
const TestWithCommons = addCommonStuff({})(Test)
;<TestWithCommons />
Reusable component with a render prop
class Common extends React.Component<CommonProps & { render?: Function, children?: Function }, $NonMaybeType<CommonProps>> {
state = {
logger: this.props.logger || new Logger(),
api: this.props.api || new MockApi(),
translate: translate
}
render() {
const { children, render } = this.props
return typeof render === 'function' ? render(this.state) : (
typeof children === 'function' ? children(this.state) : null
)
}
}
And use it like this:
class TestCommon extends React.Component<{}> {
render() {
return <Common>
{({ logger, api, translate }) => translate('namespace',
'key',
null,
`Fallback message`
)}
</Common>
}
}
As an aside to this discussion, you don't need to write defaultProps as a getter for your callout. static defaultProps = {} should be enough. It shouldn't take passed in props into account or anything. If it does, you're better off using state