Complicated touch interfaces and React

...or people have one cursor but 10 fingers.

(And why should we care.)

About me

Head of Engineering at Vazco

Since 2016 with React on production

Everything is at radekmie.dev

Happy owner of 10 fingers

So many possibilities!

  1. Pinch

  2. Pan

  3. Tap

  4. Double tap

  5. Press

  6. Drag and drop

  7. (API)

And too many solutions!

onAuxClick, onAuxClickCapture, onClick, onClickCapture, onDoubleClick, onDoubleClickCapture, onDrag, onDragCapture, onDragEnd, onDragEndCapture, onDragEnter, onDragEnterCapture, onDragOver, onDragOverCapture, onDragStart, onDragStartCapture, onDrop, onDropCapture, onMouseDown, onMouseDownCapture, onMouseEnter, onMouseLeave, onMouseMove, onMouseMoveCapture, onMouseOut, onMouseOutCapture, onMouseUp, onMouseUpCapture, onPointerCancel, onPointerCancelCapture, onPointerDown, onPointerDownCapture, onPointerEnter, onPointerLeave, onPointerMove, onPointerMoveCapture, onPointerOut, onPointerOutCapture, onPointerUp, onPointerUpCapture, onScroll, onScrollCapture, onTouchCancel, onTouchCancelCapture, onTouchEnd, onTouchEndCapture, onTouchMove, onTouchMoveCapture, onTouchStart, onTouchStartCapture, onWheel, onWheelCapture

<Canvas> or DOM

  • Easy integration
  • Event handling
  • Full CSS support
  • Rendered using GPU
  • Scales indefinitely

SVG!

<Canvas> or DOM

export function Room({ room, tables }) {
  return (










  );
}

Nothing crazy

 
 
    <svg viewBox={`0 0 ${room.width} ${room.height}`}>
      <g>






      </g>
    </svg>
 
 
 
 
 
 
        {room.visuals.map((visual, index) => (
          <RoomVisual key={index} visual={visual} />
        ))}

        
        
        
 
 
 
 
 






        {tables.map(table => (
          <Table table={table} key={table._id} />
        ))}
 
 
 
 

<Canvas> or DOM

export function RoomVisual({ visual }) {

  
  
  
  
  
  
  
  
}

Really, nothing special

 
  if (visual.type === 'circ') {
    return <circle cx={...} cy={...} r={...} />;
  }

    
    
    
    
    
    
 




  if (visual.type === 'rect') {
    return <rect height={...} width={...} />;
  }


 
 








  // ...
 

Where's event handling?

Actions = functions

import { useState } from 'react';

export function RoomInteractive(props) {

  
  
  
}

Interactions = events

 


  const state = useState(initialRoomState);



 
 



  const actionsRef = useActions(state, props);


 
 




  const svgRef = useInteractions(actionsRef, props);

 
 





  return <Room ref={svgRef} {...} />;
 

Actions

function useActions(state, props) {

  
  
  
  
  
  
  
  
  
  
  
}
 
  const actions = {
    instances: useRef({}),
    onClockClick(reservationId) {
      // `props` and `state` are always up-to-date here.
    },
    onClocksPress(selectedTableModule) {},
    onSeatClick(tableId, seatIndex) {}
  };




 
 










  actionsRef.current = actions;

 
 









  const actionsRef = useRef(actions);


 
 











  return actionsRef;
 

Stable reference to functions and instances

Interactions

 

function useInteractions(actionsRef, props) {
  const svgRef = createRef();

  
  
  
  
  
  
  
  
  
  
  return svgRef;
}
 



  useEffect(() => {

    
    
    
    
    
    
    
    
  }, [props.room._id]);
 
 
import svgPanZoom from 'svg-pan-zoom';




    const { instances } = actionsRef.current;
    instances.svgElement = svgRef.current;
    instances.svgPanZoom = svgPanZoom(svgRef.current, {
      customEventsHandler: {
        init: () => 
        destroy: () => 
      },
    });
    return () => instances.svgPanZoom.destroy();


 
 








                    instances.hammer = initHammer(...),
                       instances.hammer.destroy(),

      
      
      
      
      

Interactions

 

function initHammer() {

  
  
  
  
  
  
  
  
  
  
  
  
}
import Hammer from 'hammerjs';

 
  const hammer = new Hammer(svgElement, {
    recognizers: [
      [Hammer.Pan, { direction: Hammer.DIRECTION_ALL }],
      [Hammer.Pinch],
      [Hammer.Press],
      [Hammer.Tap, { event: 'doubletap', taps: 2 }],
      [Hammer.Tap],
    ],
  });




 
 











  hammer.get('doubletap').recognizeWith('tap');
  hammer.get('tap').requireFailure('doubletap');


 
 













  hammer.on('press', event => /* ... */);
  // ...
 

Summary

  1. Plain React components render SVG (just like HTML)
  2. svg-pan-zoom handles moving around (panning) and zooming
  3. Hammer.js detects the events and calls our handlers
  4. Clear separation of the rendering and event handling

Wait, what about props?

The event handler doesn't know which component (e.g., table) got clicked exactly

<g data-table={table._id} data-type="table">
  ...
</g>
const table = event.target.closest('[data-type=table]');
if (!table) { return; }

const tableId = table.dataset.table;

Is it still React!?

I

couldn't

care

🥰

less

Questions?