React setting default state - reactjs

Im looking for some guidance in terms of setting the default state in react.
I have a simple project using typescript & react that allows files to be dropped onto a div.
I'm trying to store these files in the state but I'm having trouble setting the default state.
export interface IAppState{
value:string;
droppedFiles: FileList
}
constructor(props: {}) {
super(props);
this.state{
value:'',
droppedFiles?:???
}
}
public render(): JSX.Element {
return (
<div className="App">
<div className="Padding">
<FileDrop onDrop={this.handleDrop}>
Drop some files here!
</FileDrop>
</div>
</div>
);
}
private handleDrop = (files:FileList, event:ReactDragEvent<HTMLDivElement>) => {
this.setState({
droppedFiles:files
});
console.log(this.state.droppedFiles);
}
If the droppedFiles is removed from the this.state step it obviously flags up as can't find the value.
due to TypeScript's type safety, it won't accept {}. What's the proper way to initialize / set the default values for a complex type?
My main issue is that I'm faced with an error from typescript because I don't know how to initialize the state.droppedFiles property with the correct data. I don't want to assign any data to it in the constructor, I'll do that when the user actually drops files.
I'm just looking for the correct way to let TS know that the state has a property of 'droppedFiles' which is of type 'FileList':
TS Error
Without this part it throws an error at runtime which I would expect:
Error
So to close it off, It turns out that FileList isn't a File[].
to get around this I updated my Interface:
export interface IAppState{
droppedFiles: File[];
}
I initialized it in my constructor like so:
constructor(props: {}) {
super(props);
this.state = {
droppedFiles: []
}
}
and finally to update my state I extracted each item from the FileList:
private handleDrop = (files:FileList, event:ReactDragEvent<HTMLDivElement>) => {
// Create an array from the FileList (as this is an array-like object)
const filesArr: File[] = Array.from(files);
// Update the state with our new array
this.setState({
droppedFiles: filesArr
});
}
This has sorted my issue. Thanks for putting me on the right path estus. Kudos.

The question cannot be extrapolated to any case. In this specific case if initial value for a property cannot be provided, a property should be made optional:
export interface IAppState{
value:string;
droppedFiles?: FileList
}
And this implies that in places where droppedFiles is used it can be either FileList or undefined, it may require if (this.state.droppedFiles) ... checks, etc.
Another option in this specific case is to not use FileList object (which is array-like) and store an array of File objects instead:
export interface IAppState{
value:string;
droppedFiles: File[]
}
...
this.state = { droppedFiles: [], ... }
...
this.setState({ droppedFiles: Array.from(files) });

Related

How to design the React component properly?

Hi I am pretty new to react and I want to achieve something that turns out to be real pain for me and I am not sure what the best solution might be.
I got an application that handles a single keystore as application keystore and the admin is allowed to upload new keystores and merge the entries of these keystores into the application keystore. This results in the following class definition:
export default class ApplicationKeystore extends React.Component
{
constructor(props)
{
super(props);
this.scimResourcePath = "/scim/v2/Keystore";
this.state = {};
this.setAliasSelectionResponse = this.setAliasSelectionResponse.bind(this);
this.onAliasSelectionSuccess = this.onAliasSelectionSuccess.bind(this);
}
setAliasSelectionResponse(resource)
{
let copiedResource = JSON.parse(JSON.stringify(resource));
this.setState({aliasSelectionResponse: copiedResource})
}
onAliasSelectionSuccess(resource)
{
this.setState({newAlias: resource[ScimConstants.CERT_URI].alias})
}
render()
{
return (
<React.Fragment>
<KeystoreUpload scimResourcePath={this.scimResourcePath}
setAliasSelectionResponse={this.setAliasSelectionResponse} />
<AliasSelection scimResourcePath={this.scimResourcePath}
aliasSelectionResponse={this.state.aliasSelectionResponse}
onCreateSuccess={this.onAliasSelectionSuccess} />
<KeystoreEntryList scimResourcePath={this.scimResourcePath}
newAlias={this.state.newAlias} />
</React.Fragment>
)
}
}
My problem now occurs on the last component KeystoreEntryList. The upload succeeds and the selection of entries to be merged works also correctly (just one at a time not several). But if I successfully merge an entry I get a response with the alias of the keystore entry that should now be added to the state of KeystoreEntryList. How do I do this in a clean way. I found several workarounds but all of them are dirty and make the code hard to read...
the important part of the KeystoreEntryListcan be found here:
class KeystoreEntryList extends React.Component
{
constructor(props)
{
super(props);
this.state = {aliases: []};
this.setState = this.setState.bind(this);
this.scimClient = new ScimClient(this.props.scimResourcePath, this.setState);
this.onDeleteSuccess = this.onDeleteSuccess.bind(this);
}
async componentDidMount()
{
let response = await this.scimClient.listResources();
if (response.success)
{
response.resource.then(listResponse =>
{
this.setState({
aliases: new Optional(listResponse.Resources[0]).map(val => val.aliases)
.orElse([])
})
})
}
}
componentDidUpdate(prevProps, prevState, snapshot)
{
// TODO if we delete an alias and add the same again the following if condition prevents adding it again
if (prevProps.newAlias !== this.props.newAlias && this.props.newAlias !== undefined)
{
let aliases = this.state.aliases;
aliases.push(this.props.newAlias);
aliases.sort();
this.setState({aliases: aliases, aliasDeleted: undefined});
}
}
...
}
my first idea was to do it on the componentDidUpdate method but this results in the problem stated out by the TODO. Is there any good way how I might be able to smoothly add the new alias into this component?
You should not mutate the state directly as below
let aliases = this.state.aliases;
aliases.push(this.props.newAlias);
In React when the state is an [] or {} . React just holds the reference of it and check whether the reference change . Since you are pushing a value to the aliases the reference of aliases will not changes which makes react to think nothing has changed and it will not trigger a re-render.
Instead of pushing , you need to create a new copy of the array
const newAliases = [...this.state.alias, this.props.alias];
newAliases.sort;
this.setState({aliases: newAliases, ...})

Where should a state be defined?

What is the difference between these two constructs of defining state in React?
class ProductsPage extends Component {
constructor(props) {
super(props);
this.state = {
products: []
};
}
...
}
and this:
class ProductsPage extends Component {
state = {
products: []
};
...
}
Both of them work well when coded in ES6. However, the lower one doesn't seem to work in typescript 3. I have the following:
interface IState {
products: IProduct[];
}
class ProductsPage extends Component<{}, IState> {
state = {
products: []
};
public componentDidMount() {
this.setState({ products });
}
public render() {
return (
<div className="page-container">
<ul className="product-list">
{this.state.products.map(p => (
<li className="product-list-item" key={p.id}>
{p.name}
</li>
))}
</ul>
</div>
);
}
}
and the ts compiler flagged an error saying: Property id does not exist on type 'never'
Why is that?
The second form is class properties which is a stage 3 JavaScript proposal (meaning it's not part of the language yet). Adding properties on the constructor is the old ES2015 way (called maximally minimal classes).
In your case there is no functional difference.
TypeScript requires you to declare class fields for type safety - hence the warning.
How to define the state for a React component is as subjective as the coding styles React promotes itself. Usually I go the very strict route which looks as follows:
type State = {
someStateVar: number[];
};
export class MyComponent extends Component<{}, State> {
public readonly state: State = {
someStateVar: [],
};
public async componentDidMount() {
// Dynamically fetch data (maybe via axios) and populate the new state
const data = await axios.get<number[]>(...);
this.setState({ someStateVar: data });
}
}
As you can see, I explicitly mark state as readonly just make sure, nobody attempts to write directly to it (even though IDEs and linters can check for those errors without the precaution nowadays).
Another reason why I prefer to not set the state manually with an assigment is that it might encourage wrong handling of state. You are never to assign something directly to state in a class method without the use of setState.
Furthermore I am basically defining the defaults right away and populating the state with dynamic data in componentDidMount. This approach allows you to keep the code concise by dropping an explicit constructor definition as you should move such an initialization to componentDidMount anyways and use the constructor only for binding methods if you don't use the class member arrow notation for those in the first place.

Is Initializing state with props object causes mutation?

In my React application, one of the components needs state initialization from props.
Class ComponentA extends React.Component{
constructor(props){
this.state = {
objectA: props.objectA
}
}
someOnclickFunction(e){
let updatedObjA = this.state.objectA;
updatedObjA.value = e.target.value;
this.setState({
objectA: updatedObjA
})
}
}
In the above code snippet, props.objectA reference is copied to state. So, Am I mutating the props indirectly by updating the state?
Or setState() function will clone the object and keep new reference for the objectA?
class ComponentA extends React.Component {
constructor(props) {
super(props);
// state is null at this point, so you can't do the below.
// this.state.objectA = props.objectA
// instead, initialize the state like this:
this.state = {
objectA: props.objectA,
};
}
someOnclickFunction(e) {
// you can't set "objectA.value" like the following
// this.setState({
// objectA.value: e.target.value
// });
// you need to create a new object with your property changed, like this:
this.setState({
objectA: Object.assign({}, this.state.objectA, { value: e.target.value }),
})
}
}
This is a mistake that many beginners at react make. You can't simply update sub-properties of an object without consequences.. The following would also be wrong:
someOnclickFunction(e) {
var tmp = this.state.objectA;
// WRONG: don't do this either, you're modifying the state by doing this.
tmp.value = e.target.value;
this.setState({
objectA: tmp,
});
}
To elaborate on the correct way to do this using Object.assign.. this function takes all the parameters and merges them into the first element. So by providing a new object as the first parameter, you've created a copy of your first object with your new property.
Object.assign({}) // = {}
Object.assign({}, objectA) // = copy of objectA
Object.assign({}, objectA, { value: "newValue" }) // = copy of objectA with 'value' = 'newValue'.
Note: Object.assign() is a shallow clone, not a deep clone.

Is the constructor still needed in React with autobinding and property initializers

I am refactoring an es6 class based React component that uses the normal constructor, and then binds methods, and defines state/attributes within that constructor. Something like this:
class MySpecialComponent extends React.Component {
constructor(props) {
super(props)
this.state = { thing: true }
this.myMethod = this.myMethod.bind(this)
this.myAttribute = { amazing: false }
}
myMethod(e) {
this.setState({ thing: e.target.value })
}
}
I want to refactor this so that I am autobinding the functions, and using property initializers for the state and attributes. Now my code looks something like this:
class MySpecialComponent extends React.Component {
state = { thing: true }
myAttribute = { amazing: false }
myMethod = (e) => {
this.setState({ thing: e.target.value })
}
}
My question is, do I still need the constructor? Or are the props also autobound? I would have expected to still need the constructor and included super(props), but my code seems to be working and I'm confused.
Thanks
From my understanding, you don't need to type out a constructor at all when using class properties (as in your second code example). The accepted answer states that you do need one if you "need to reference the props in your initial state object," but if you're using said class properties, then you're probably using Babel to transpile it, in which case a constructor is used, it's just being done behind the scenes. Because of this, you don't need to add a constructor yourself, even if you are using props in state.
See this aricle for better examples and a better explanation.
You don't need an explicitly defined constructor unless you need to reference the props in your initial state object.
You don't need to define a constructor explicitly , and then do super(props).You can access the props as in the example below. i.e. 'prop1'
class MySpecialComponent extends React.Component {
state = {
thing: true ,
prop1:this.props.prop1
}
myAttribute = { amazing: false }
myMethod = (e) => {
this.setState({ thing: e.target.value })
}
render(){
console.log(this.state.prop1);
return(
<div>Hi</div>
);
}
}
ReactDOM.render(<MySpecialComponent prop1={1}/> , mountNode);

React: Define constants?

I have a container component which is feeding a presentation component with one image at a time (every time a user clicks a button). I want to define those image paths in a constant (array). But I get TypeError: Cannot read property '1' of undefined when trying to read from that array.
How can I solve that error, and is this the best approach?
export default class ImageContainer extends Component {
constructor(props){
super(props)
this.state = {
currentImage: '/images/dummy-image-1.jpg'
}
this.changeImage = this.changeImage.bind(this)
const images = [
'/images/dummy-image-1.jpg',
'/images/dummy-image-2.jpg',
'/images/dummy-image-3.jpg',
'/images/dummy-image-4.jpg',
'/images/dummy-image-5.jpg'
]
}
changeImage(event){
console.log(this.images[1])
//Do something
}
render(){
return(
<ImageView image={this.state.currentImage} changeImage={this.changeImage} />
)
}
}
You need to define images as object property in constructor:
this.images = [
'/images/dummy-image-1.jpg',
...
];
when you declare variables (using var, let, const) it is visible only in function (in your case constructor) scope.
You could move the const definition outside of the class:
const images = [
'/images/dummy-image-1.jpg',
'/images/dummy-image-2.jpg',
'/images/dummy-image-3.jpg',
'/images/dummy-image-4.jpg',
'/images/dummy-image-5.jpg'
];
export default class ImageContainer extends Component {
//...
}
This way images remains a const and it's still only visible to the code within the file (which I'll assume is just the ImageContainer class).

Resources