React Konva share ref between sibling components throws error - reactjs

I have App.tsx which contains 2 sibling components:
Konva.tsx: It has the Canvas
Options.tsx: It has a Download Canvas button
So I created a ref named stageRef in App.tsx to pass it to Konva.tsx & Options.tsx. I use React.forwardRef to forward refs to child components.
App.tsx
import * as React from 'react'
import type { Stage as StageType } from 'konva/types/Stage'
import { Konva, Options } from '#/components/index'
import { FrameItProvider } from '#/store/index'
const App = () => {
const stageRef = React.createRef<StageType>()
return (
<>
<Konva ref={stageRef} />
<Options ref={stageRef} />
</>
)
}
export default App
In Konva.tsx, the ref points to the Canvas so it can access the element in the DOM.
Konva.tsx
import * as React from 'react'
import { observer } from 'mobx-react'
import { useFrameItStore } from '#/store/index'
import { BrowserWindow } from '#/components/index'
import type { Window } from '#/types/index'
import type { Stage as StageType } from 'konva/types/Stage'
interface IProps {
className?: string
}
export const Konva = observer(
React.forwardRef<StageType, IProps>(({ className }: IProps, forwardedRef) => {
const frameItStore = useFrameItStore()
const browser: Window = frameItStore.browser
return (
<>
<Stage
width={browser.width}
height={browser.height}
ref={forwardedRef}
className={className}
>
<Layer>
<BrowserWindow />
</Layer>
</Stage>
</>
)
})
)
In Options.tsx, I trigger the download call using downloadImage with the forwardedRef.
Options.tsx
import * as React from 'react'
import { observer } from 'mobx-react'
import type { Stage as StageType } from 'konva/types/Stage'
import { useFrameItStore } from '#/store/index'
import type { TrafficSignalStyle } from '#/types/index'
interface IProps {
className?: string
}
export const Options = observer(
React.forwardRef<StageType, IProps>((props: IProps, forwardedRef) => {
const frameItStore = useFrameItStore()
const downloadImage: (stageRef: React.ForwardedRef<StageType>) => void =
frameItStore.downloadImage
return (
<div>
<button onClick={() => downloadImage(forwardedRef)}>
Download Canvas
</button>
</div>
)
})
)
I'm using MobX to manage my store. However, the forwardRef causes problem.
store/index.ts
import type { Stage as StageType } from 'konva/types/Stage'
import type { IFrameItStore } from '#/types/index'
export class FrameItStore implements IFrameItStore {
downloadImage(stageRef: React.ForwardedRef<StageType>) {
console.log(stageRef)
stageRef
.current!.getStage()
.toDataURL({ mimeType: 'image/jpeg', quality: 1 })
}
}
types/index.ts
export interface IFrameItStore {
downloadImage(stageRef: React.ForwardedRef<StageType>): void
}
I get 2 TypeScript errors in store/index.ts:
TS2531: Object is possibly 'null'.
on stageRef when I try to access stageRef.current and
TS2339: Property 'current' does not exist on type '((instance: Stage | null) => void) | MutableRefObject<Stage | null>'.
Property 'current' does not exist on type '(instance: Stage | null) => void'.
on current
I tried not using ForwardedRef but it gave error that the types do not match so I have to use ForwardedRef but I'm not sure how to solve this?

I made a ref in App.tsx & then passed it to Konva.tsx. I catched the ref using React.forwardRef function.
Then I used the lesser known useImperativeHandle hook in Konva.tsx to access the function downloadImage in App.tsx & then pass the function downloadImage directly to Options.tsx.
I didn't keep anything in MobX store. Just kept it locally in Konva component where its easily accessible. The old rule is that if it isn't going to be used in any other component, then it should be kept as close to the component as possible.
App.tsx
import * as React from 'react'
import { Konva, Options } from '#/components/index'
const App = () => {
const stageRef = React.useRef<{ downloadImage: Function }>(null)
return (
<>
<Konva ref={stageRef} />
<Options downloadImage={() => stageRef?.current?.downloadImage()} />
</>
)
}
export default App
Konva.tsx
import * as React from 'react'
import { observer } from 'mobx-react'
import { useFrameItStore } from '#/store/index'
import { BrowserWindow } from '#/components/index'
import type { Window } from '#/types/index'
import type { Stage as StageType } from 'konva/types/Stage'
interface IProps {
className?: string
}
interface ForwardedRef {
downloadImage: Function
}
export const Konva = observer(
React.forwardRef<ForwardedRef, IProps>(
({ className }: IProps, forwardedRef) => {
const frameItStore = useFrameItStore()
const browser: Window = frameItStore.browser
const stageRef = React.useRef<StageType>(null)
React.useImperativeHandle(
forwardedRef,
() => ({
downloadImage: () =>
stageRef.current
?.getStage()
.toDataURL({ mimeType: 'image/jpeg', quality: 1 }),
}),
[]
)
return (
<Stage
width={browser.width}
height={browser.height}
ref={stageRef}
className={className}
>
<Layer>
<BrowserWindow />
</Layer>
</Stage>
)
}
)
)
Options.tsx
import * as React from 'react'
import { observer } from 'mobx-react'
interface IProps {
className?: string
downloadImage: Function
}
export const Options = observer((props: IProps) => {
const { downloadImage } = props
return (
<div>
<button onClick={() => console.log(downloadImage())}>
Download Image
</button>
</div>
)
})
Here's a complete working demo on CodeSandBox → https://codesandbox.io/s/react-konva-share-refs-between-siblings-dsd97?file=/src/App.tsx

Related

Problem using React createContext and typescript

I'm working on a simple calculator made on react + typescript using vite. It's my first time using react + typescript so I don't know the right way to use the ContextAPI. I got this error trying to build the aplication with yarn build.
src/components/Screen.tsx:5:11 - error TS2339: Property 'showOnScreen' does not exist on type '{}'.
const { showOnScreen, onDeleteButton } = useContext(AppContext);
AppContext.tsx
import { createContext } from 'react';
export const AppContext = createContext({});
ContextProvider.tsx
import { useState } from 'react';
import { AppContext } from './';
type Props = {
children: React.ReactNode;
};
export const ContextProvider: React.FC<Props> = ({ children }) => {
const [showOnScreen, setShowOnScreen] = useState('0');
const [isLastAResult, setIsLastAResult] = useState(false);
const onDeleteButton = () => {
if (showOnScreen.length === 1 || isLastAResult) {
setShowOnScreen('0');
setIsLastAResult(false);
return;
}
setShowOnScreen(showOnScreen.substring(0, showOnScreen.length - 1));
};
return (
<AppContext.Provider
value={{
onDeleteButton,
showOnScreen,
}}
>
{children}
</AppContext.Provider>
);
};
Screen.tsx
import { useContext } from 'react';
import { AppContext } from '../context';
export const Screen = () => {
const { showOnScreen, onDeleteButton } = useContext(AppContext);
return (
<div className='row bg-white rounded-4'>
<div className='col-12 pe-0 d-flex justify-content-between align-items-center'>
<span className='py-2 fs-4'>{showOnScreen}</span>
<button
className='text-danger rounded-start btn rounded-4 h-100 fs-5'
onClick={onDeleteButton}
>
DEL
</button>
</div>
</div>
);
};
In TypeScript, createContext uses generics ie it is a createContext<T> that automatically adapts to your type when you don't specify anything else. It propagates the type to your variable - AppContext which gets assigned {} as a type.
There are various ways to assign it a type, like:
export const AppContext = createContext({} as { showOnScreen?: bool; });
or
export const AppContext = createContext<{ showOnScreen?: bool; }>({});
or
interface IContext {
showscreen?: bool;
}
export const AppContext = createContext<IContext>({});

Create wrapper for translation with MUI - WEBPACK_IMPORTED_MODULE_16__ is undefined

I use react-i18next in my project to provide proper translations, in most components I import
const { t } = useTranslation();
to provide proper content however I wanted to write custom translationWrapper where I would use useTranslation hook and just reuse that component where I need. However it keeps throwing error:
Uncaught TypeError: TranslationWrapper__WEBPACK_IMPORTED_MODULE_16_
is undefined
Why does it happen? Interesting is that if project is running and I add it then it works correctly but when I refresh browser then get above error
My Custom wrapper:
import React, { FC } from 'react';
import { Typography, TypographyProps } from '#mui/material';
import { TFunction } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import { styled } from '#mui/styles';
import { TOptions } from 'i18next';
export type TextContentPath = Parameters<TFunction<'translation'>>[0];
export interface TranslationWrapperProps extends TypographyProps {
content?: TextContentPath;
tParams?: TOptions<{}>;
onClick?: () => void;
}
const TranslationWrapperComponent: FC<TranslationWrapperProps> = ({ content, tParams, onClick, ...materialProps }) => {
const { t } = useTranslation();
return (
<Typography {...materialProps} {...onClick}>
{t(content as TemplateStringsArray, tParams)}
</Typography>
);
};
export const TranslationWrapper = React.memo(TranslationWrapperComponent) as typeof TranslationWrapperComponent;
and try to reuse it here:
export const MyComponent: React.FC = () => {
return (
<StyledSwitchBox>
<StyledTester
variant="p"
sx={{ cursor: 'pointer' }}
onClick={() => console.log('test')}
content={'myComponent.switchContent'}
/>
</StyledSwitchBox>
);
};
const StyledTester = styled(TranslationWrapper)({});

Api Context props Boolean don't turn

What I try to do
I try to use apiContext with ThemeProvider, when the user want switch a light mode to dark mode, my state doesn't want change.
My operative mode
I create an interface themeContext files for manage types and initialize context.
A themeContext file for call createContext and made a custom hook.
A ThemeProvider for pass my constant 'theme' is always a boolean and an a method for switch it.
In my template I have a part of logic for call the hook and mutate turn the theme.
My console return always false, what's going wrong. I deliberately omitted react's imports.
My code
IthemeContext.ts
export interface IThemeContext {
theme: boolean;
toggleTurn?: () => void;
}
export const defaultThemeState: IThemeContext = {
theme: false,
toggleTurn: () => !defaultThemeState.theme
}
themeContext.ts
import {defaultThemeState, IThemeContext} from "../interface/IThemeContext";
const ThemeContext = React?.createContext<Partial<IThemeContext>>(defaultThemeState);
export const useThemeContext = () => useContext(ThemeContext);
export default ThemeContext;
ThemeProvider
(…)
import ThemeContext, {useThemeContext} from "../context/themeContext";
export const ThemeProvider: FC<{children: ReactNode}> = ({ children}) => {
const {theme, toggleTurn} = useThemeContext();
return (
<ThemeContext.Provider value={
{
theme,
toggleTurn
}
}>
{ children }
</ThemeContext.Provider>
)
}
export default ThemeProvider;
App.tsx
(…)
import Template from "./component/template";
import {ThemeProvider} from "./component/provider/theme.provider";
function App() {
return (
<ThemeProvider>
<Template />
</ThemeProvider>
)
}
export default App
Template.tsx
(…)
const Template: FC = (): ReactElement<any, any> | null => {
const { theme, toggleTurn } = useThemeContext();
const toggleTheme = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (toggleTurn) {
toggleTurn();
console.log(theme);
}
};
return (
(…)
<button type="button" onClick={
(e: React.MouseEvent<HMTLButtonElement) =>
toggleTheme(e)
}
(…)
)

React, Typescript Error: How can I solve type error? (HTMLButtonElement problem)

Greeting!
I make TodoList Sample, Use by React, Redux, Typescript, SCSS.
I have one problem about Typescript error.
Here is error status.
Type '(event: { target: HTMLButtonElement; }) => void' is not assignable to type 'MouseEventHandler<HTMLButtonElement>'.
Types of parameters 'event' and 'event' are incompatible.
Type 'MouseEvent<HTMLButtonElement, MouseEvent>' is not assignable to type '{ target: HTMLButtonElement; }'.
Types of property 'target' are incompatible.
Type 'EventTarget' is missing the following properties from type 'HTMLButtonElement': disabled, form, formAction, formEnctype, and 248 more.
I made TodoItem remove function.
So This is my code.
Container (Add onClickRemoveTodo)
TodosContainer
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import TodoAdd from 'src/components/todo/TodoAdd';
import TodoList from 'src/components/todo/TodoList';
import { RootState } from 'src/redux/rootReducer';
import { changeTodo, clearTodo } from 'src/redux/todo/todo';
import { removeTodo, setAddNewTodo } from 'src/redux/todo/todos';
const TodosContainer = () => {
const dispatch = useDispatch();
const { todo, todos } = useSelector((state: RootState) => ({
todo: state.todo,
todos: state.todos,
}));
const onChangeTodoAdd = (event: { target: HTMLInputElement }) => {
const { name, value } = event.target;
dispatch(changeTodo({ name, value }));
};
const onClickTodoAdd = () => {
dispatch(setAddNewTodo({ todo }));
dispatch(clearTodo());
};
// I made onClickRemoveTodo
const onClickRemoveTodo = (event: { target: HTMLButtonElement }) => {
const { value } = event.target;
dispatch(removeTodo({ id: parseInt(value, 10) }));
};
return (
<>
<h3>TodosContainer</h3>
<TodoAdd
todo={todo}
onChange={onChangeTodoAdd}
onClick={onClickTodoAdd}
/>
{/* // Pass to TodoList (onClickRemoveTodo -> onRemove) */}
<TodoList
todos={todos}
onRemove={onClickRemoveTodo}
/>
</>
);
};
export default TodosContainer;
Components (Add onRemove)
TodoList
import React from 'react';
import { Todo } from 'src/types/todo';
import { isEmpty } from 'src/utils/tools';
import TodoItem from './TodoItem';
export type TodoListProps = {
todos: Todo[];
onRemove: (event: { target: HTMLButtonElement }) => void;
};
const TodoList = ({ todos, onRemove }: TodoListProps) => {
if (isEmpty(todos.length)) {
return (
<>
<p>Todo List Empty</p>
</>
);
}
return (
<>
<h4>TodoList</h4>
{
todos.map(({ id, content }) => (
// Pass to TodoItem (onRemove -> onRemove)
<TodoItem
key={id}
id={id}
content={content}
onRemove={onRemove}
/>
))
}
</>
);
};
export default TodoList;
TodoItem
import React from 'react';
import { isEmpty } from 'src/utils/tools';
export type TodoItemProps = {
id: number;
content: string;
// I think this is problem.
onRemove: (event: { target: HTMLButtonElement }) => void;
};
const TodoItem = ({ id, content, onRemove }: TodoItemProps) => {
if (isEmpty(id)) {
return (
<></>
);
}
return (
<>
<p>{content}</p>
<button
type="button"
// FIXME: need to solve Typesript error case, but logic work well.
onClick={onRemove}
value={id}
>
remove
</button>
</>
);
};
export default TodoItem;
So i made onClickRemoveTodo function from TodosContainer and it pass TodosContainer -> TodoList -> TodoItem.
Also Logic has no problem. It works(remove Todo) in webpage.
But suddenly TodoItem Component / onClick={onRemove} show type error.
I try to find solution, but i had no clue this.
Here is my repository and you can read detail code.
https://github.com/DavidYang2149/react_redux_typescript_scss_starter
Thank you for reading this. :)
===========================================================================
Solved
Container (Add onClickRemoveTodo)
TodosContainer
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import TodoAdd from 'src/components/todo/TodoAdd';
import TodoList from 'src/components/todo/TodoList';
import { RootState } from 'src/redux/rootReducer';
import { changeTodo, clearTodo } from 'src/redux/todo/todo';
import { removeTodo, setAddNewTodo } from 'src/redux/todo/todos';
const TodosContainer = () => {
const dispatch = useDispatch();
const { todo, todos } = useSelector((state: RootState) => ({
todo: state.todo,
todos: state.todos,
}));
const onChangeTodoAdd = (event: { target: HTMLInputElement }) => {
const { name, value } = event.target;
dispatch(changeTodo({ name, value }));
};
const onClickTodoAdd = () => {
dispatch(setAddNewTodo({ todo }));
dispatch(clearTodo());
};
// Fixed :)
const onClickRemoveTodo = ({ id }: { id: number }) => {
dispatch(removeTodo({ id }));
};
return (
<>
<h3>TodosContainer</h3>
<TodoAdd
todo={todo}
onChange={onChangeTodoAdd}
onClick={onClickTodoAdd}
/>
<TodoList
todos={todos}
onRemove={onClickRemoveTodo}
/>
</>
);
};
export default TodosContainer;
Components (Add onRemove)
TodoList
import React from 'react';
import { Todo } from 'src/types/todo';
import { isEmpty } from 'src/utils/tools';
import TodoItem from './TodoItem';
export type TodoListProps = {
todos: Todo[];
// Fixed :)
onRemove: ({ id }: { id: number }) => void;
};
const TodoList = ({ todos, onRemove }: TodoListProps) => {
if (isEmpty(todos.length)) {
return (
<>
<p>Todo List Empty</p>
</>
);
}
return (
<>
<h4>TodoList</h4>
{
todos.map(({ id, content }) => (
<TodoItem
key={id}
id={id}
content={content}
onRemove={onRemove}
/>
))
}
</>
);
};
export default TodoList;
TodoItem
import React from 'react';
import { isEmpty } from 'src/utils/tools';
export type TodoItemProps = {
id: number;
content: string;
// Fixed :)
onRemove: ({ id }: { id: number }) => void;
};
const TodoItem = ({ id, content, onRemove }: TodoItemProps) => {
if (isEmpty(id)) {
return (
<></>
);
}
return (
<>
<p>{content}</p>
<button
type="button"
// Fixed :)
onClick={() => onRemove({ id })}
>
remove
</button>
</>
);
};
export default TodoItem;
Thank you.
Try to change all your definition of onRemove to MouseEventHandler.
import { MouseEventHandler } from 'react';
onRemove: MouseEventHandler<HTMLButtonElement>;
For onclick function
import { MouseEvent } from 'react';
const onClickRemoveTodo = (event: MouseEvent) => {//you code...}

Error: Objects are not valid as a React child (found: object with keys {})

I'm a beginner learning ts for the first time. Thank you in advance for sharing your knowledge. I am making a to-do list. I used to react to complete it. But now I am using react and typescript together to complete the code.
I got an error. I don't know what the problem is. Help me, please.
Error: Objects are not valid as a React child (found: object with keys {}). If you meant to render a collection of children, use an array instead.
Click here to view the full code
What I think that the problem is this file.
// contet.tsx
import React, { createContext, useReducer, useContext, Dispatch } from 'react';
import reducer from "./reducer";
import { Action } from './actions'
export interface ITodo {
id: string;
text: string;
};
export interface State {
toDos: ITodo[];
completed: ITodo[];
}
interface ContextValue {
state: State;
dispatch: Dispatch<Action>;
}
export const initialState = {
toDos: [],
completed: [],
};
const ToDosContext = createContext<ContextValue>({
state: initialState,
dispatch: () => { console.error("called dispatch outside of a ToDosContext Provider") }
});
export const ToDosProvider = ({ children }: { children: React.ReactNode }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<ToDosContext.Provider value={{ state, dispatch }}>
{children}
</ToDosContext.Provider>
);
};
export const useTodosState = (): State => {
const { state } = useContext(ToDosContext);
return state;
};
export const useTodosDispatch = (): Dispatch<Action> => {
const { dispatch } = useContext(ToDosContext);
return dispatch;
};
This is App.tsx
import React from "react";
import Add from "./Add";
import Title from "./Title";
import Progress from "./Progress";
import List from "./List";
import ToDo from "./ToDo";
import styled from "styled-components";
import { useTodosState } from '../context';
const App = () => {
const { toDos, completed } = useTodosState();
console.log(toDos);
return (
<Title>
<Add />
<Progress />
<Lists>
<List title={toDos.length !== 0 ? "To Dos" : ""}>
{toDos.map((toDo) => (
<ToDo key={toDo.id} id={toDo.id} text={toDo.text} isCompleted={false} />
))}
</List>
<List title={completed.length !== 0 ? "Completed" : ""}>
{completed.map((toDo) => (
<ToDo key={toDo.id} id={toDo.id} text=
{toDo.text} isCompleted />
))}
</List>
</Lists>
</Title >
)
}
export default App;
I had a look at the repo you shared the problem is at List.tsx component and the way you are trying to access your props from your components. It should be
const List = ({ title, children }: any) => (
instead of
const List = (title: any, children: any) => (
as in react functional components take only one parameter the props object.
Also if you want to add types there you could do {title:string; children: ReactElement| ReactElement[]}
I think it has being a better way for this situation. You can use PropsWithChildren you can check it out for details.
for little example
export interface SearchProps {
height: number
}
function Search({ children }: PropsWithChildren<SearchProps>) {
..
..
return()
}

Resources