How I Fix the Unexpected Token < Error That Caused Filtrr2.js to Crash in React App

I’m working on a small image editor side project with react app, and I wanted to sprinkle in some quick filters without pulling in a heavyweight library. Filtrr2 looked perfect old-school jQuery, a handful of filters, nothing fancy. But my very first run threw a wall of red “Unexpected token <” errors at me.

The Code That Broke

public/index.html

<body>
<div id="root"></div>

<!-- dependencies -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/camanjs/4.1.2/caman.full.js"></script>

<!-- Filtrr2 (my first, broken attempt) -->
<script src="../src/plugins/filtrr2.js"></script>
<script src="../src/plugins/util.js"></script>
<script src="../src/plugins/effects.js"></script>
<script src="../src/plugins/layers.js"></script>
<script src="../src/plugins/events.js"></script>
</body>

src/App.js

Import { useEffect } from 'react';

function App() {
useEffect(() => {
console.log('filtrr2 on window:', window.filtrr2); // <- prints undefined
}, []);

return <h1>Filtrr2 test</h1>;
}

export default App;

What I Saw in The Console

SyntaxError: Unexpected token <  filtrr2.js:1
Uncaught SyntaxError: Unexpected token < util.js:1
…(same for every file)…

Why the Browser Choke

  • I pointed <script> tags into src/
    Create-React-App only serves the bundled output of src. Those raw files simply don’t exist at run-time.
  • The dev server fell back to index.html
    React’s dev server uses HTML-5 history fallback. When the browser asked for /src/plugins/filtrr2.js, the server replied with the root HTML page.
  • HTML arrived where JavaScript was expected
    The first character inside that page is <. The JS parser hit it, shrugged, and threw the Unexpected token < error.
  • window.filtrr2 stayed undefined
    None of the Filtrr2 files loaded, so my console.log printed undefined.

Define Fix Code

  • Move the files: I created public/plugins and dropped these five scripts inside:
filtrr2.js
util.js
effects.js
layers.js
events.js
  • Reference them with root-relative paths:
<!-- still in public/index.html -->
<script src="%PUBLIC_URL%/plugins/filtrr2.js"></script>
<script src="%PUBLIC_URL%/plugins/util.js"></script>
<script src="%PUBLIC_URL%/plugins/effects.js"></script>
<script src="%PUBLIC_URL%/plugins/layers.js"></script>
<script src="%PUBLIC_URL%/plugins/events.js"></script>
  • %PUBLIC_URL% becomes / in dev and the correct base path in a production build.
  • Restart
    A quick npm start reload, and this time the console showed the Filtrr2 constructor instead of angry red errors.

A Tiny Working Demo

I wanted something tangible, so I built a 30-line React component that:

  • lets me pick an image,
  • drops it on a <canvas>, and
  • toggles a grayscale filter.
// src/FiltrrDemo.js
import { useRef, useState } from 'react';

export default function FiltrrDemo() {
const canvasRef = useRef(null);
const [isGray, setIsGray] = useState(false);

/* 1 — load a file onto the canvas */
const loadImage = e => {
const file = e.target.files[0];
if (!file) return;

const img = new Image();
img.onload = () => {
const cvs = canvasRef.current;
cvs.width = img.width;
cvs.height = img.height;
cvs.getContext('2d').drawImage(img, 0, 0);
setIsGray(false);
};
img.src = URL.createObjectURL(file);
};

/* 2 — apply or remove grayscale */
const toggleGray = () => {
const cvs = canvasRef.current;
if (!cvs) return;

window.filtrr2(cvs, function () {
isGray ? this.original() : this.grayscale();
this.render(() => setIsGray(!isGray));
});
};

return (
<>
<input type="file" accept="image/*" onChange={loadImage} />
<button onClick={toggleGray} disabled={!canvasRef.current}>
{isGray ? 'Remove grayscale' : 'Apply grayscale'}
</button>
<br />
<canvas ref={canvasRef} style={{ maxWidth: '100%', marginTop: 8 }} />
</>
);
}

Drop it into App.js:

FiltrrDemo from './FiltrrDemo';

function App() {
return (
<div style={{ padding: 16 }}>
<h1>Filtrr2–React demo</h1>
<FiltrrDemo />
</div>
);
}

export default App;

Open the page, pick a photo, smash the button instant gray scale.

Fun Ways to Keep Practising

  • Stack filters —swap grayscale() for sepia().brightness(20).contrast(15).
  • Make it interactive —add a range slider and pipe its value into brightness(value).
  • Save the resultcanvas.toDataURL('image/png') and trigger a download.
  • One-click presets —map buttons to filter chains like vintage, gotham, posterize.

Each tweak drives home how to wrap a legacy script in a modern React workflow.

Key Lessons I’m Taking Away

  1. Everything inside src/ is private until Webpack bundles it.
    If you need a raw file, park it in public/.
  2. “Unexpected token <” is almost always HTML masquerading as JS.
    First stop: DevTools → Network tab.
  3. No npm? No problem.
    Drop the script in public/, load it early, and treat it like any other global.
  4. Thin React wrappers keep old APIs from leaking everywhere.
    My mini component isolates Filtrr2 so the rest of my app stays declarative and testable.

Final Thought

I love when a 15-year-old piece of code still finds a home in a brand-new React app it’s a reminder that the web is one giant Lego bucket. Yes, I tripped over a path error, but the fix was painless, and now I have a slick little filter playground to keep extending. If you bump into the same “Unexpected token <” monster, check your file paths, move your scripts to public/, and keep building.

Related blog posts