Can I implement typescript conditional types by javascript object values - typescript-generics

I am writing a React editor component, it could be used to write new posts or update an existing post.If used as an update editor, the props MUST receive title and content; if used as new post editor, title and content will not exist.
But I don't know how to let typescript access the "flag" props so that it could decide which interface it could use.
I want to use the component as follows:
<PostEditor flag="newPost"/>
<PostEditor
flag="updatePost"
title="a good story"
content="once upone a time"
/>
I write the interface as follows:
interface NewPostProps{
flag:"newPost",
}
interface UpdatePostProps{
flag:"updatePost",
title:string,
content:string,
}
type IPostEditorProps<T>= T extends "newPost"?NewPostProps:UpdatePostProps
I write the react component as this. It does not work. I want the flag props to be the generic type but I don't know how to write that.
export const PostEditor=({flag,title}:IPostEditorProps<T>)=>{
// component contents
return (<></>)
}
Thank you for your help.

Working example code:
interface CommonProps {
children?: React.ReactNode;
// ...other props that always exist
}
interface NewPostProps {
content: never;
flag: "newPost";
title: never;
}
interface UpdatePostProps {
content: string;
flag: "updatePost";
title: string;
}
type ConditionalProps = NewPostProps | UpdatePostProps;
type Props = CommonProps & ConditionalProps;
export const PostEditor = (props: Props): JSX.Element => {
const { children, content, flag, title } = props;
if (flag === "newPost") return <>{/* implement newpost */}</>;
return (
<div>
<h1>{title}</h1>
<p>{content}</p>
{children}
</div>
);
};
Useful link: conditional react props with typescript
If you have any questions, feel free to ask!

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

Reactjs custom hook call infinite loop

I am creating my first ever self made web development project and have run into an infinite loop. The full project can be found on https://github.com/Olks95/my-dnd/tree/spellbook. So the question is: What causes the loop and how do I fix it?
(The loop happens somewhere in the 2nd item of the 'Playground' component when the ContentSelector - Spellbook is called. The custom hook useHandbook is called in Spellbook and continously calls the API, should obviously only happen once... refresh or click return to stop spamming )
From what I can tell the issue is not in the custom hook itself, as I have made several attempts to rewrite it and an empty dependency array is added to the end of the useEffect(). I will try to explain with example code here.
import { Component1, Component2, Component3 } from './ContentSelector.js';
const components = {
option1: Component1,
option2: Component2
option3: Component3
}
const Playground = (props) => {
const LeftItem = components['option1']
const MiddleItem = components['option2']
const RightItem = components['option3']
...
}
I wanted to be able to choose what content to put in each element and ended up making a ContentSelector component that has all the content components in one file, and individually imported/exported. This seems like a strange way to do it, but it was the only way I found to make it work. (Maybe the cause of the loop?) Since this is still fairly early on in the development the selection is hard coded. The item variables starts with a capital letter so I can later call them as components to render like so:
<LeftItem ...some properties... />
Playground then returns the following to be rendered:
return(
<React.Fragment>
<div className="container">
<div className="flex-item">
/* Working select-option to pass correct props to Component1 */
<div className="content">
<LeftItem ...some properties... />
</div>
</div
<div className="flex-item">
/* Currently the same selector that changes the content of the LeftItem */
<div className="content">
<MiddleItem ...some properties... />
</div>
</div>
/*RightItem follows the same formula but currently only renders "coming soon..." */
</div>
</React.Fragment>
)
The Content selector then has the three components where:
Component1: calls a custom hook that only runs once. The information is then sent to another component to render. All working fine.
Component2: calls a custom hook infinite times, but is expected to work the same way component 1 does...
Component3: Renders coming soon...
See Component1 and 2 below:
export const Component1 = (props) => {
const [ isLoading, fetchedData ] = useDicecloud(props.selectedChar);
let loadedCharacter = null;
if(fetchedData) {
loadedCharacter = {
name: fetchedData[0].Name,
alignment: fetchedData[0].Alignment,
/* a few more assignments */
};
}
let content = <p>Loading characters...</p>;
if(!isLoading && fetchedData && fetchedData.length > 0) {
content = (
<React.Fragment>
<Character
name={loadedCharacter.name}
alignment={loadedCharacter.alignment}
/* a few more props */ />
</React.Fragment>
)
}
return content;
}
export const Component2 = (props) => {
const [ fetchedData, error, isLoading ] = useHandbook('https://cors-anywhere.herokuapp.com/http://dnd5eapi.co/api/spells/?name=Aid')
let content = <p>Loading spells...</p>;
if(!isLoading && fetchedData) {
/* useHandbook used to return text to user in a string in some situations */
if(typeof fetchedData === 'string') {
content = (
<React.Fragment>
<p> {fetchedData} </p>
</React.Fragment>
)
} else {
content = (
<React.Fragment>
<Spellbook
/* the component that will in the future render the data from the API called in useHandbook */
/>
</React.Fragment>
)
}
}
return content;
}
I have been working on this issue for a few days and it is getting more confusing as I go along. I expected the mistake to be in useHandbook, but after many remakes it does not seem to be. The current useHandbook is very simple as shown below.
export const useHandbook = (url) => {
const [ isLoading, setIsLoading ] = useState(false);
const [ error, setError ] = useState(null);
const [ data, setData ] = useState(null);
const fetchData = async () => {
setIsLoading(true);
try {
const res = await fetch(url, {
method: "GET",
mode: 'cors'
});
const json = await res.json();
setData(json);
setIsLoading(false);
} catch(error) {
setError(error);
}
};
useEffect(() => {
fetchData();
}, []); //From what the documentation says, this [] should stop it from running more than once.
return [ data, error, isLoading ];
};
EDIT: I ran the chrome developer tools with the react extension and saw something that might be useful:
Image showing Component2 (Spellbook) run inside itself infinite times
You can modify your custom hook useHandbook's useEffect hook to have URL as dependency, since useEffect is similar to componentWillMount, componentDidUpdate and componentWillUnmount, in your case it is componentDidUpdate multiple times. So what you can do is.
useEffect(() => {
fetchData();
}, [url]);
Since there is no need to fetch data agin unless URL is changed
I found the mistake. Component2's real name was Spellbook which I had also called the rendering component that I had not yet made. It turns out I was calling the component from within itself.
Easy to see in the image at the edit part of the question.

mapStateToProps and mapDispatchToProps: getting IDE to "see" the props

Problem: IDE does not resolve props passed to the component via connect()
Note: this is not a bug, but an inconvenience to the coder
Say I have this React component connected to Redux via connect():
class SomeComponent extends Component {
render() {
return (
{this.props.someObject ? this.props.someObject : ''}
);
}
}
function mapStateToProps(state) {
return {
someObject: new SomeObject(state.someReducer.someObjectInfo),
};
}
function mapDispatchToProps(dispatch) {
return {
// ...
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatsList);
I'm using the IntelliJ IDE, and any prop connected to the component in the above manner, such as someObject, will get an unresolved variable warning. And if someObject has some properties/methods, they will neither be resolved nor show up in code suggestions (which are really helpful).
A workaround
Pass state and dispatch themselves as props:
function mapStateToProps(state) {return {state};}
function mapDispatchToProps(dispatch) {return {dispatch};}
Define my variables in the constructor (as opposed to via props):
constructor(props) {
this.someVar = props.state.someReducer.someVar;
this.someObj = new SomeObject(props.state.someReducer.someObjectInfo;
}
Update the variables manually whenever props change:
componentWillReceiveProps(nextProps) {
someObject.update(nextProps.state.someReducer.someObjectInfo);
}
The drawback is having additional boilerplate logic in componentWillReceiveProps, but now the IDE happily resolves the variables and code suggestion works.
Question
Is the workaround preferable? I'm using it, like it so far, and have not observed any other drawbacks thus far. Is there a better way to get the IDE to understand the code?
Motivation (verbose; only for those interested in why I want to accomplish the above)
The Redux tutorials show a simple way to connect state/dispatch to props, e.g.:
function mapStateToProps(state) {
users: state.usersReducer.users
chats: state.chatsReducer.chats
}
function mapDispatchToProps(dispatch) {
addUser: (id) => dispatch(usersActions.addUser(id))
addChatMsg: (id, msg) => dispatch(chatsActions.addChatMsg(id, msg)
}
In the example above, the coder of a component will need to know every relevant reducers' names and their state variables. This can get messy for the coder. Instead, I want to abstract these details away from the component. One way is with a "module" class that accepts state and dispatch, and provides all get/set methods:
class Chats {
// Actions
static ADD_MESSAGE = "CHATS/ADD_MESSAGE";
constructor(globalState, dispatch) {
this.chatsState = globalState.chats;
this.dispatch = dispatch;
}
// Get method
getChats() {
return this.chatsState.chats;
}
// Set method
addChatMessage(id, msg) {
return this.dispatch({
type: Chats.ADD_MESSAGE,
id,
msg
};
}
// Called by componentWillReceiveProps to update this object
updateChats(nextGlobalState) {
this.chatsState = nextGlobalState.chats;
}
}
Now, if a Component requires the Chats module, a coder simply does this:
class SomeComponent extends Component {
constructor(props) {
this.chats = new Chats(props.state, props.dispatch);
}
componentWillReceiveProps(nextProps) {
this.chats.updateChats(nextProps);
}
// ...
}
And now, all Chats get/set methods and properties will be available, and will be picked up by the IDE.
I think newest Idea can now understand component properties defined via propTypes and provides code completion for them. So you just declare propTypes. And it is not even a workaround, it's a good practice in my opinion.
class ChatsList extends Component {
static propTypes = {
someObject: PropTypes.shape({
color: PropTypes.string,
someFunc: PropTypes.func
}),
someDispatcher: PropTypes.func
};
render() {
return (
{this.props.someObject ? this.props.someObject : ''}
);
}
}
function mapStateToProps(state) {
return {
someObject: new SomeObject(state.someReducer.someObjectInfo),
};
}
function mapDispatchToProps(dispatch) {
return {
someDispatcher: Actions.someDispatcher
// ...
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatsList);
Also, passing the entire state is a bad idea, since a component will receive props and get re-renderend if anything changes in the entire state (unless you provide shouldComponentUpdate)

React Komposer + Container Pattern + Data Input

I'm now working with react-komposer and the container/component pattern, but it's left me wondering how to handle data input.
For example, an AddVehicleForm component has a container that pre-populates some fields with data from the database. With the standard React Komposer examples, this makes sense:
import { composeWithTracker } from 'react-komposer';
import { Vehicles } from '../../collections/vehicles.js';
import AddVehicleForm from '../components/AddVehicleForm.jsx';
const composer = ( props, onData ) => {
const subscription = Meteor.subscribe( 'vehicles' );
if ( subscription.ready() ) {
const curVehicles = Vehicles.find().fetch();
onData( null, { curVehicles } );
}
};
const Container = composeWithTracker( composer )( AddVehicleForm );
But, to keep the component truly unreliant on it's data source, you would also need to pass it a handleSubmit() function to submit to the database, would you not? Where would you put this function?
Alternatively, I can see how it wouldn't be hard to solve using TrackerReact. But, as React Komposer is so widely adopted, what's the common way to handle this case?
EDIT:
Just throwing out an idea, but is there any reason not to create a container component with submit handling methods and then wrap that with the composer function? Something akin to this:
import {composeWithTracker} from 'react-komposer';
import ClassroomDashboard from '/imports/components/classroomDashboard/ClassroomDashboard.jsx';
class ClassroomDashboardContainer extends React.Component {
onSubmitHandle(e) {
// check form data and submit to DB
}
render() {
return(
<ClassroomDashboard {...this.props} onSubmit={this.onSubmitHandle.bind(this)} />
)
}
}
function composerFunction(props, onData) {
const handle = Meteor.subscribe('classroom');
if (handle.ready()) {
const classroom = Classrooms.findOne(props.params.id);
onData(null, {classroom});
};
};
export default composeWithTracker(composerFunction)(ClassroomDashboardContainer);

Unexpected Token after const

I am getting an unexpected token error in React when I try to specify a constant, and I cannot seem to figure out why.
My code is pretty simple, and I have followed the react-bootstrap examples here almost exactly.
My code is as follows:
import { Component, PropTypes } from 'react';
var rbs = require('react-bootstrap'),
Panel = rbs.Panel;
export default class ResumeSection extends Component {
constructor(...args) {
super(...args);
this.state = {
open: true
};
}
const title = (
<h3>Panel title</h3>
);
render() {
return (
<Panel collapsible expanded={this.state.open}>
<p>Body</p>
</Panel>
);
}
}
The error occurs on title directly after const and just says SyntaxError: Unexpected Token
You can't define a const in the class body like that; it should be moved into a method.
render() {
const title = (
<h3>Panel title</h3>
);
// ...
}
Apparently, this is called "Public Class Field Syntax" and is already available in babel as the plugin, babel-plugin-transform-class-properties. I have not tried it as yet though.
Additional reference is reactjs.org events guide

Resources