Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeError: this.state.draggedStyle is null #109

Open
Audiopolis opened this issue Sep 2, 2021 · 2 comments
Open

TypeError: this.state.draggedStyle is null #109

Audiopolis opened this issue Sep 2, 2021 · 2 comments

Comments

@Audiopolis
Copy link

Audiopolis commented Sep 2, 2021

UPDATE 3
Unfortunately, I was unable to find the root cause. If you see an issue with my code, please let me know. I will try to find the time to make a minimal, reproducible example. The fact that draggedStyle is set correctly every second mouse event leads me to believe that this is a bug in react-reorder. The only workaround I found involved forking react-reorder, I opted to use react-dnd instead, which is a lot more complicated, but it works.

UPDATE 2
I couldn't tell you the root problem. I'm also having a different issue causing me to be unable to debug the application (connection refused), so I'm relying on console.log. Logging every time draggedStyle is written to with something vs. null, I would not expect it to be null when this error occurs, but it is. Every second time. However, this seems to fix the issue:

if (this.state.draggedStyle === null) {
  console.log("Skipping because draggedStyle is null.");
  return;
}

this.state.draggedStyle.transform = createOffsetStyles(event, this.props);
store.setDraggedStyle(this.props.reorderId, this.props.reorderGroup, this.state.draggedStyle);

This seems to have no side-effects, and reordering, drag and drop are perfectly smooth.

My only issue now is one that I also had before this: If I drag an item from "left-side" to "right-side" or vice versa, the component disappears from the source list and its "ghost" (my translucent clone placeholder) appears in the destination list (as expected) while still dragging. When I release it, it goes back to its source list, in the same position. Note that both lists have the same components.

UPDATE 1
Second time writing this update for some reason. Props are set correctly, and the stylesheet is also loaded. The names match up, and the bug has nothing to do with draggedClassName. I noticed something weird when inspecting state where the error occurs: It's only sometimes undefined. I narrowed it down to not being set on children in render:

// index.js
var isDragged = this.isDragging() && this.isDraggingFrom() && index === this.state.draggedIndex;
var draggedStyle = isDragged ? assign({}, child.props.style, this.state.draggedStyle) : child.props.style;

In the above code, draggedStyle will be undefined if child.props.style is undefined and isDragged is false. The child would be my WidgetWrapper, which in the default state has no style. I thought the fix would be:

          var childStyle = child.props.style ?? {};  // <-- here
          var draggedStyle = isDragged ? assign({}, childStyle, this.state.draggedStyle) : childStyle;

But this still isn't working. The expectation that style.transform is defined also needs to be dealt with. Still investigating.

ORIGINAL BODY
Edit: Irrelevant info removed
I'm using react-reorder with Typescript and self-declared types (please add types!). Before migrating to Typescript, it was working fine. Now, when I drag a component, I get this output in the console:

Uncaught TypeError: this.state.draggedStyle is null
    onWindowMove index.js:647
    componentDidMount index.js:688
    chainedFunction factory.js:741
    React 6
    unstable_runWithPriority scheduler.development.js:468
    React 9
    tsx index.tsx:7
    tsx main.chunk.js:2609
    Webpack 7
        __webpack_require__
        fn
        1
        __webpack_require__
        checkDeferredModules
        webpackJsonpCallback
        <anonymous>

I'm using a Reorder component with draggedClassName={widgetStyles.Dragged}, where widgetStyles is a sass stylesheet imported as widgetStyles. widgetStyles.Dragged resolves to something like WidgetWrapper_Dragged__3VyWh, but even using a regular css stylesheet with just .dragged { styles go here } and using "dragged" as draggedClassName results in the same error.

The console prints the error on every mouse event (edit: turns out to be every second mouse event) and it's very laggy. Traceback excerpt:

  644 | 
  645 | }
  646 | 
> 647 | this.state.draggedStyle.transform = createOffsetStyles(event, this.props);
      | ^  648 | store.setDraggedStyle(this.props.reorderId, this.props.reorderGroup, this.state.draggedStyle);
  649 | 
  650 | mouseOffset = {
@JakeSidSmith
Copy link
Owner

Hey. 😁

Can you let me now which version you're using?
When the error is seen? (is it when the cursor moves after starting a drag?)
And if possible, whip up an example of your code?

Since this has only started happening since you moved to typescript then I imagine the issue could be caused by something in your build/typescript config (however there are a number of things that could have caused it).

Side note: I plan to re-write this lib in typescript, just haven't found the need/time yet. 😊

@Audiopolis
Copy link
Author

Audiopolis commented Sep 2, 2021

Thanks for the response!

Can you let me now which version you're using?

^3.0.0-alpha.7

When the error is seen? (is it when the cursor moves after starting a drag?)

Yes; the error is seen when the cursor moves after a drag has been started. It only occurs after the hold time has passed and the component is actually mobile.

And if possible, whip up an example of your code?

Sure, here's the relevant code (can't give access to the repo):

// src/components/LayerInfo/LayerInfo.tsx
import React from "react";
import styles from "./LayerInfo.module.sass";
import widgetStyles from "../WidgetWrapper/WidgetWrapper.module.sass";
import {ForegroundContainer} from "../../shared/components/ForegroundContainer/ForegroundContainer";
import Reorder, {reorder, reorderFromToImmutable, reorderImmutable} from "react-reorder";

import {WidgetWrapper} from "../WidgetWrapper/WidgetWrapper";
import {IssueProgressWidget} from "../../widgets/IssueProgressWidget/IssueProgressWidget";
import {EmptyWidget} from "../../widgets/EmptyWidget/EmptyWidget";
// import "./dragged.css";

interface Props {
    reorder_group_id: string
}

// TODO: This is duplicated
interface ListItem {
    index: number
    widget_type: string
}

interface State {
    list: Array<ListItem>
    [name: string]: any
}

// TODO: See https://github.com/JakeSidSmith/react-reorder/issues/101#issuecomment-847201025
class LayerInfo extends React.Component<Props, State> {
    state: Readonly<State> = {
        list: []
    }

    constructor(props: Props) {
        super(props);
        this.onReorder = this.onReorder.bind(this);
        // Example  TODO: Database
        let list: Array<ListItem> = [
            {
                index: 0,
                widget_type: "IssueProgressWidget",
            },
            {
                index: 1,
                widget_type: "EmptyWidget",
            },
        ];

        this.state = {
            list: list
        }
    }

    renderWidget(item: ListItem) {
        // TODO: Use database
        switch (item.widget_type) {
            case "EmptyWidget":
                return (
                    <EmptyWidget/>
                );
            case "IssueProgressWidget":
                return (
                    <IssueProgressWidget/>
                );
            default:
                // TODO: Error
                return (
                    <div>Hello :)</div>
                );
        }
    }

    render() {
        return (
            <div className={styles.FlexContainer}>
                <ForegroundContainer
                    title={"Widgets"}
                    margin_bottom={true}
                    margin_top={true}
                    flex={true}
                    widgets={true}
                    padding={8}
                    scroll={true}
                >
                    <Reorder
                        reorderId={this.props.reorder_group_id} // Unique ID that is used internally to track this list (required)
                        reorderGroup="widgets" // A group ID that allows items to be dragged between lists of the same group (optional)
                        // getRef={this.storeRef.bind(this)} // Function that is passed a reference to the root node when mounted (optional)
                        component="div" // Tag name or Component to be used for the wrapping element (optional), defaults to 'div'
                        placeholderClassName={widgetStyles.Placeholder}
                        // draggedClassName={"dragged"}
                        draggedClassName={widgetStyles.Dragged}
                        // lock="horizontal" // Lock the dragging direction (optional): vertical, horizontal (do not use with groups)
                        touchHoldTime={500} // Hold time before dragging begins on touch devices (optional), defaults to holdTime
                        mouseHoldTime={500} // Hold time before dragging begins with mouse (optional), defaults to holdTime
                        onReorder={this.onReorder.bind(this)} // Callback when an item is dropped (you will need this to update your state)
                        autoScroll={false} // Enable auto-scrolling when the pointer is close to the edge of the Reorder component (optional), defaults to true
                        disabled={false} // Disable reordering (optional), defaults to false
                        disableContextMenus={true} // Disable context menus when holding on touch devices (optional), defaults to true
                    >
                        {
                            this.state.list.map((item: ListItem) => (
                                <div key={item.index}>
                                    <WidgetWrapper>
                                        {this.renderWidget(item)}
                                    </WidgetWrapper>
                                </div>
                            ))
                        }
                    </Reorder>
                </ForegroundContainer>
            </div>
        )
    }

    onReorder (event: any, previousIndex: number, nextIndex: number, _fromId: string, _toId: string) {
        this.setState({
            list: reorder(this.state.list, previousIndex, nextIndex)
        });
        // TODO: Save
    }

    onReorderGroup (event: any, previousIndex: number, nextIndex: number, fromId: string, toId: string) {
        if (fromId === toId) {
            const list = reorderImmutable(this.state[fromId], previousIndex, nextIndex);

            this.setState({
                [fromId]: list
            });
        } else {
            const lists = reorderFromToImmutable({
                from: this.state[fromId],
                to: this.state[toId]
            }, previousIndex, nextIndex);

            this.setState({
                [fromId]: lists.from,
                [toId]: lists.to
            });
        }
    }
}

export {LayerInfo}

My tsconfig.json:

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": "./",
    "noImplicitAny": false  // TODO: Remove
  },
  "include": [
    "src"
  ],
  "typesRoot": [
    "node_modules/@types",
    "src/@custom_types/react-reorder"
  ],
  "exclude": [
    "node_modules",
    "src/@custom_types"
  ]
}

src/components/WidgetWrapper/WidgetWrapper.tsx:

import React, {useCallback, useEffect} from "react";
import styles from "./WidgetWrapper.module.sass";

const WidgetWrapper = (props: any) => {
    // Whether or not to mirror react-reorder's behavior of resetting the hold time when the mouse moves
    const useReoderRules = true;

    // States
    const [isMouseDown, setIsMouseDown] = React.useState(false);
    const [isHovering, setIsHovering] = React.useState(false);
    const [mouseLeftDuringPress, setMouseLeftDuringPress] = React.useState(false);
    const [waitingForDone, setWaitingForDone] = React.useState(false);

    // Event handlers
    const onMouseDown = () => {
        setIsMouseDown(true);  // Fact
    }
    const onMouseUp = () => {
        setIsMouseDown(false);  // Fact
    }
    // NOTE: onMouseEnter does not bubble and would be unreliable for widgets with child elements.
    const onMouseMove = useCallback(() => {
        if (isHovering) {
            if (isMouseDown && useReoderRules) {
                // react-reorder resets the hold time when the mouse is moved during a drag. Use the else clause if this is
                // ever fixed (I suppose it's a matter of personal preference).
                setMouseLeftDuringPress(true);
                setIsMouseDown(false);
            } else if (mouseLeftDuringPress) {
                // Require continuous press; pretend like the button was released.
                setMouseLeftDuringPress(false);
                setIsMouseDown(false);
            }
        } else {
            setIsHovering(true);
        }
    }, [isHovering, isMouseDown, mouseLeftDuringPress, useReoderRules])

    const onMouseLeave = () => {
        setIsHovering(false);
        setMouseLeftDuringPress(isMouseDown);
    }

    const onMouseEnter = () => {
        setIsHovering(true);
    }

    // Misc
    const reset = () => {
        // NOTE: Do not set isHovering = false (the state of isHovering is always semantically true and accurate)
        setIsMouseDown(false);
        setMouseLeftDuringPress(false);
        setWaitingForDone(false);
        setWaitingForDone(false);
    }

    // Logic

    let pressedIndicatorClsNames = [styles.PressedIndicator];
    let pressing = false;
    
    if (mouseLeftDuringPress) {
        pressedIndicatorClsNames.push(styles.PressedUnknown);
    } else if (isMouseDown && isHovering) {
        pressing = true;
        pressedIndicatorClsNames.push(styles.Pressed);
    }

    let wrapperClsNames = [styles.WidgetWrapper]
    if (isHovering) { wrapperClsNames.push(styles.Hovering); }

    useEffect(() => {
        if (pressing && !waitingForDone) {
            setTimeout(() => {
                onMouseMove();
                reset();
            }, 500);
            setWaitingForDone(true);
        }
    }, [pressing, waitingForDone, onMouseMove])
    return (
        <div
            className={wrapperClsNames.join(" ")}
            onMouseDown={_e => onMouseDown()}
            onMouseUp={_e => onMouseUp()}
            onMouseLeave={_e => onMouseLeave()}
            onMouseEnter={_e => onMouseEnter()}
            onMouseMove={_e => onMouseMove()}
        >
            {props.children}
            <div className={pressedIndicatorClsNames.join(" ")}/>
        </div>
    )
}

export {WidgetWrapper}

WidgetWrapper.module.sass:

.WidgetWrapper
  width: 100%
  background-color: white
  min-height: 50px
  user-select: none
  box-sizing: border-box
  position: relative
  border-radius: 5px
  border: 1px solid gray
  z-index: 1
  margin-bottom: 10px

.PressedIndicator
  width: 0
  height: 8px
  bottom: -4px
  left: -1px
  right: 1px
  z-index: -1
  background-color: #1a85ff
  content: ''
  transition: width
  border-bottom-left-radius: 5px  // TODO: Display below parent element somehow
  position: absolute

.Hovering  // TODO: Try to remove
  z-index: 2

.Placeholder
  @extend .WidgetWrapper
  background-color: transparent
  box-shadow: 0 0 5px rgba(26, 133, 255, 0.7)
  opacity: 0.2

// For when we know for sure that the component is being pressed (building up to drag).
// Applies to the "pressed indicator."
.Pressed
  width: 100%
  transition-delay: 150ms
  transition-duration: 250ms

// For when we don't know if the component is being dragged because the mouse left the component.
// Applies to the "pressed indicator."
.PressedUnknown
  width: 0
  z-index: 2
  transition-duration: 0ms

.Dragged > .WidgetWrapper
  border: 1px solid #1a85ff
  z-index: 2

I'm new to Typescript, so you're probably right about the issue being something I'm doing wrong. It's unlike any other typed language I've used.

(Almost) everything should be up to date, as I (recently) created the app using the latest version of create-react-app with the typescript template and node.js, only adding latest (or latest stable) versions of packages.

Side note: I plan to re-write this lib in typescript, just haven't found the need/time yet. 😊

Totally understand. Even just adding type declarations for now would help, ie.:
(Note that my type declarations are not great and that they only work for my project specifically, but they allow me to use react-reorder without disabling all type checking globally.)

// index.d.ts
declare module "react-reorder" {
    // TODO: This is duplicated
    export interface ListItem {
        index: number
        widget_type: string
    }

    export function reorder(list: Array<ListItem>, prevIndex: number, nextIndex: number): Array<ListItem>;
    export function reorderImmutable(item: ListItem, prevIndex: number, nextIndex: number): Array<ListItem>;
    export function reorderFromToImmutable(from_to: Object, prevIndex: number, nextIndex: number): { from: string, to: string };
    // @ts-ignore  TODO: Fix
    import React from 'react';

    export interface ReorderComponentProps {
        reorderId: string  // Unique ID that is used internally to track this list (required)
        reorderGroup?: string  // A group ID that allows items to be dragged between lists of the same group (optional)
        getRef?: Function  // Function that is passed a reference to the root node when mounted (optional)
        component?: string  // Tag name or Component to be used for the wrapping element (optional), defaults to 'div'
        placeholderClassName: string  // The class name to use for styling the placeholder
        draggedClassName: string  // The class name to use for styling dragged components
        lock?: boolean  // Lock the dragging direction (optional): vertical, horizontal (do not use with groups)
        touchHoldTime?: number  // Hold time before dragging begins on touch devices (optional), defaults to holdTime
        mouseHoldTime?: number  // Hold time before dragging begins with mouse (optional), defaults to holdTime
        onReorder: Function  // Callback when an item is dropped (you will need this to update your state)
        autoScroll?: boolean  // Enable auto-scrolling when the pointer is close to the edge of the Reorder component (optional), defaults to true
        disabled?: boolean  // Disable reordering (optional), defaults to false
        disableContextMenus?: boolean  // Disable context menus when holding on touch devices (optional), defaults to true
        children?: any
    }

    const Reorder: (props: ReorderComponentProps) => React.ReactElement<ReorderComponentProps>;
    export default Reorder;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants