How to sync a generic type between a parent and child component in flow? - flowtype

I have two components that work in tandem (parent child relationship) and they must share generic types. So when you instantiate the parent with one specific type, all the child components must be restricted to take only that type on their generic.
So for example, I want this to fail:
<Parent onChange={console.log} value={5}>
<Child value="5"/>
</Parent>
However, I haven't been able to make that work (AKA fail). I look at flow docs about abstract component but I don't think that can help in this scenario.
This is a small reproduction of what I have tried so far:
//#flow
import type { ChildrenArray, Element } from 'react'
import React, { Children, type Node } from 'react'
type ParentProps<T> = {
onChange: (value: T) => void,
value: T,
children: ChildrenArray<Element<typeof Child>>,
}
export function Parent<T>({
children,
value,
onChange,
}: ParentProps<T>) {
const childComponents = Children.map(children, (child) =>
React.cloneElement(child, {
onSet: () => {
onChange(child.props.value)
},
})
)
return <div>{childComponents}</div>
}
type ChildProps<T> = {
children?: Node,
value: T,
onSet?: () => void,
}
const Child = <T>({children, onSet}: ChildProps<T>) => {
return <button onClick={onSet}>{children}</button>
}
<Parent onChange={console.log} value={5}><Child value="5"/></Parent>
Here is the link to the TryFlow reproduction

Related

An array of valid CSS property names in TypeScript, but hyphened

I have a property in my component that is supposed to get an array of valid CSS properties:
interface MyComponentProps {
cssProperties: (keyof CSSStyleDeclaration)[];
}
const MyComponent = ({ cssProperties }: MyComponentProps) => {
//
}
The problem is that CSSStyleDeclaration stores styles as an object with property names in camel case. I need real CSS property values, hyphened. So background-color instead of backgroundColor. I know there's also React.CSSProperties type, but it uses camel-cased properties too, while allowing for unitless numeric values.
Is there a TypeScript type to use original, hyphened CSS property names?
There is this library csstype which is used by MUI, emotion and some other popular component libraries.
Which allows you to use the hyphen props
import CSS from "csstype";
interface MyComponentProps {
cssProperties: (keyof CSS.PropertiesHyphen)[];
}
const MyComponent = ({ cssProperties }: MyComponentProps) => {
//
}
<MyComponent cssProperties={["color", "background-color"]} />;
Use Kebab type available here. Camelcased CSS properties will be transformed into kebabcase.
type Kebab<T extends string, A extends string = ""> =
T extends `${infer F}${infer R}` ?
Kebab<R, `${A}${F extends Lowercase<F> ? "" : "-"}${Lowercase<F>}`> :
A
type Props = {
cssProps: Kebab<keyof React.CSSProperties>[];
}
const Component = (props: Props) => null;
const App = () => (
<Component cssProps={['min-width']} />
);
Typescript Playground

easy-peasy computed does not take inputs

The Redux library easy-peasy has a function called computed which is an alternative to standard Redux selectors:
import { computed } from 'easy-peasy'
const model = {
session: {
totalPrice: computed(state => state.price + state.tax)
}
}
And then the selector is called in the component like this:
import { useStoreState } from 'easy-peasy'
function TotalPriceOfProducts() {
const totalPrice = useStoreState(state => state.products.totalPrice)
return <div>Total: {totalPrice}</div>
}
The problem is that there doesn't seem to be a way to pass inputs to the selector. If I need a specific object in an array in the state, I can't pass the ID of the object as an input. My only option is to do this on the component side. Redux selectors have the advantage of being functions, so I can pass inputs to be used in the selector logic.
Anyone use easy-peasy come across this problem before?
I can't pass the ID of the object as an input
To enable this, you have to return a function as your computed property.
Here is an example, exposing a computed function getTodoById:
import { computed } from 'easy-peasy'
const model = {
todos: [],
getTodoById: computed(state => {
// Note how we are returning a function instead of state
// 👇
return (id) => state.todos.find(t => t.id === id)
})
}
Then you can use it like this:
import { useStoreState } from 'easy-peasy'
function Todo({ id }) {
const getTodoById = useStoreState(state => state.getTodoById)
const matchingTodo = getTodoById(id);
// ... etc
}

How to pass host component's CSS class to children?

I cannot understand how to pass host component CSS class to a children element. I created a custom element:
...
const CUSTOM_INPUT_VALUE_PROVIDER: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormFieldComponent),
multi: true,
}
#Component({
moduleId: module.id,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [CUSTOM_INPUT_VALUE_PROVIDER],
selector: 'form-field',
template: `
<div>
<input
(change)="onChange($event.target.value)"
(blur)="onTouched()"
[disabled]="innerIsDisabled"
type="text"
[value]="innerValue" />
</div>
`
})
export class FormFieldComponent implements ControlValueAccessor {
#Input() innerValue: string;
innerIsDisabled: boolean = false;
onChange = (_) => {};
onTouched = () => {};
writeValue(value: any) {
if (value !== this.innerValue) {
this.value = value;
}
}
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean) {
this.innerIsDisabled = isDisabled;
}
get value(): any {
return this.innerValue;
}
set value(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
this.onChange(value);
}
}
}
And then use it like this in some reactive form:
<form-field formControlName="title"></form-field>
Problem: I added some validation in FormBuilder to title form control and when it not pass validation, Angular add classic css classes to form-field element: ng-pristine ng-invalid ng-touched.
How i can pass this CSS classes from host element to my input element in form-field component?
It is not duplicate of Angular 2 styling not applying to Child Component. Changing Encapsulation does not resolve the problem.
I think there's a way to do what you want by just knowing the angular classes of the hosting elements and not necessarily passing them down.
If so, your work-around would look something like this in the css of the custom form element:
:host.(ng-class)>>>HTMLelement {
property: value
}
Example:
:host.ng-valid>>>div {
border-left: 5px solid #42a948;
}
The ":host" part of this allows us to use the hosting (parent) html elements
The ">>>" is the deep selector that allows us to apply these styles to all children matching selection property (in this case, we're looking for div elements)

react-native navigating between screens from non component class

I'm trying to navigate between react native screens from my Backend class like this:
var self = this;
firebase.auth().onAuthStateChanged((user) => {
if (user) {
self.setState({
userID: user.uid,
})
} else{
self.props.navigation.navigate("Login");
}
});
My backend class is not a component and therefore is not imported into the stack navigator I am using. I am getting an error saying 'self.props.navigation is not an object'.
Does anyone know I can fix this? Thanks
One not-so-good practice is to define your Navigator as a static/class variable of your App instance:
const MyNavigator = StackNavigator(...);
export default class MyApp extends Component {
render() {
return <MyNavigator ref={(ref) => MyApp.Navigator = ref}/>
}
}
then you can access your navigator and it's props and functions anywhere you want! (for example dispatch a back event):
import MyApp from '...';
MyApp.Navigator.dispatch(NavigationActions.back());
I am personally not a fan of navigation actions happening at that level however, sometimes it's necessary. Expanding on the answer from #Dusk a pattern was made known to me that helps with this very solution. You can find it here
https://github.com/react-community/react-navigation/issues/1439#issuecomment-303661539
The idea is that you create a service that holds a ref to your navigator. Now from anywhere in your app you can import that service and have access to your navigator. It keeps it clean and concise.
If you are using react-navigation then you can achieve this via Navigation Service
Create a file named NavigationService and add the below code there
import { NavigationActions, StackActions } from 'react-navigation';
let navigator;
function setTopLevelNavigator(navigatorRef) {
navigator = navigatorRef;
}
function navigate(routeName, params) {
navigator.dispatch(
NavigationActions.navigate({
routeName,
params
})
);
}
function goBack(routeName, params) {
navigator.dispatch(
StackActions.reset({
index: 0,
actions: [
NavigationActions.navigate({
routeName,
params
})
]
})
);
}
function replace(routeName, params) {
navigator.dispatch(
StackActions.replace({
index: 0,
actions: [
NavigationActions.navigate({
routeName,
params
})
]
})
);
}
function pop() {
navigator.dispatch(StackActions.pop());
}
function popToTop() {
navigator.dispatch(StackActions.popToTop());
}
// add other navigation functions that you need and export them
export default {
navigate,
goBack,
replace,
pop,
popToTop,
setTopLevelNavigator
};
Now import this file in your app.js and set the TopLevelNavigator, your app.js will look something like this
import React, { Component } from 'react';
import NavigationService from './routes/NavigationService';
export default class App extends Component {
constructor() {
super();
}
render() {
return (
<View style={{ flex: 1, backgroundColor: '#fff' }}>
<AppNavigator
ref={navigatorRef => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
/>
</View>
);
}
}
Now you are good to go, you can import your NavigationService where ever you want, you can use it like this in any of the components and non-component files
import NavigationService from 'path to the NavigationService file';
/* you can use any screen name you have defined in your StackNavigators
* just replace the LogInScreen with your screen name and it will work like a
* charm
*/
NavigationService.navigate('LogInScreen');
/*
* you can also pass params or extra data into the ongoing screen like this
*/
NavigationService.navigate('LogInScreen',{
orderId: this.state.data.orderId
});

Redux pattern for populating the state, and then building components from that state?

I've got a component that builds search/sort filters that can be selected. I want the selected state of those filters to be tracked in redux so that the search builder can subscribe and see when they change and update appropriately. the thing I'm trying to figure out how to do (in a way that doesn't feel weird) is populate the filter objects into the state. Eg, right now in the <Search /> component I have something like:
<OptionPicker
group={'searchFilters'}
options={{word: 'price', active: true},
{word: 'distance', active: false},
{word: 'clowns', active: false}}
/>
So how to get those props into state to be used without triggering multiple element renders. I'm also rendering the app on the server as well, so for the initial attachment render, the state already has the options.
In the OptionPicker component I've got:
class OptionPicker extends Component {
constructor(props) {
super(props)
if (!props.optionstate) {
this.props.addOptionState(props)
}
}
render() {
return {this.props.optionstate.word.map((word) => <Option ... />)}
}
}
function mapStateToProps(state, props) {
return {
optionstate: state.optionstate[props.group],
};
}
function mapDispatchToProps(dispatch) {
return {
addOptionState: (props) => {
dispatch(addOptionState(props));
},
optionToggled: (group, word) => {
dispatch(updateOptionState(group, word));
}
};
}
export default connect(mapStateToProps,mapDispatchToProps)(OptionGroup);
This kinda works, but there exists a time when render is called before the redux state has been populated, which throws an error. I could guard against that, but none of this feels "right". Shouldn't that prop always be there? Is there a pattern for this that I'm missing?
I agree with you in that the prop should always be there. The pattern I use for this is to set up an initial state and to pass it to the reducer function:
export const INITIAL_STATE = {
optionstate: { /* all filters are present but deactivated */ }
};
export default function (state = INITIAL_STATE, action) {
// reduce new actions into the state
};

Resources